diff --git a/.github/workflows/luacheck.yml b/.github/workflows/luacheck.yml index a03fe92..d00f53a 100644 --- a/.github/workflows/luacheck.yml +++ b/.github/workflows/luacheck.yml @@ -1,6 +1,6 @@ name: luacheck -on: [push] +on: [push, pull_request] jobs: build: diff --git a/README.md b/README.md index bd40e4d..31a15c6 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,13 @@ To provide a web-based interface to receive/send mails you can use the [mtui](ht To access your mail click on the inventory mail button or use the "/mail" command Mails can be deleted, marked as read or unread, replied to and forwarded to another player +# Compatibility / Migration + +Overview: +* `v1` all the data is in the `/mails.db` file +* `v2` every player has its own (in-) mailbox in the `/mails/.json` file +* `v3` every player has an entry in the `` modstorage (inbox, outbox, contacts) + # Dependencies * None @@ -43,6 +50,12 @@ See the "LICENSE" file * Cheapie (initial idea/project) * Rubenwardy (lua/ui improvements) +* BuckarooBanzay (cleanups, refactoring) +* Athozus (outbox, maillists, ui fixes) +* fluxionary (minor fixups) +* SX (various fixes) +* Toby1710 (ux fixes) +* Peter Nerlich (cc, bcc) # Old/Historic stuff * Old forum topic: https://forum.minetest.net/viewtopic.php?t=14464 diff --git a/api.lua b/api.lua index 1559954..24d1a47 100644 --- a/api.lua +++ b/api.lua @@ -10,36 +10,15 @@ end mail.receive_mail_message = "You have a new message from %s! Subject: %s\nTo view it, type /mail" mail.read_later_message = "You can read your messages later by using the /mail command" ---[[ -mail sending function, can be invoked with one object argument (new api) or -all 4 parameters (old compat version) -see: "Mail format" api.md +function mail.send(m) + if type(m.from) ~= "string" then return false, "'from' is not a string" end + if type(m.to) ~= "string" then return false, "'to' is not a string" end + if type(m.body) ~= "string" then return false, "'body' is not a string" end -TODO: refactor this garbage code! ---]] -function mail.send(...) - -- figure out format - local m - if #{...} == 1 then - -- new format (one table param) - m = ... - -- populate "to" field - m.to = m.to or m.dst - -- populate "from" field - m.from = m.from or m.src - else - -- old format - m = {} - m.from, m.to, m.subject, m.body = ... - end + -- defaults + m.subject = m.subject or "(No subject)" - -- sane default values - m.subject = m.subject or "" - m.body = m.body or "" - - if m.subject == "" then - m.subject = "(No subject)" - end + -- limit subject line if string.len(m.subject) > 30 then m.subject = string.sub(m.subject,1,27) .. "..." end @@ -47,11 +26,14 @@ function mail.send(...) -- normalize to, cc and bcc while compiling a list of all recipients local recipients = {} local undeliverable = {} + m.to = mail.concat_player_list(mail.extractMaillists(m.to, m.from)) m.to = mail.normalize_players_and_add_recipients(m.to, recipients, undeliverable) if m.cc then + m.cc = mail.concat_player_list(mail.extractMaillists(m.cc, m.from)) m.cc = mail.normalize_players_and_add_recipients(m.cc, recipients, undeliverable) end if m.bcc then + m.bcc = mail.concat_player_list(mail.extractMaillists(m.bcc, m.from)) m.bcc = mail.normalize_players_and_add_recipients(m.bcc, recipients, undeliverable) end @@ -60,9 +42,7 @@ function mail.send(...) for name in pairs(undeliverable) do undeliverable_names[#undeliverable_names + 1] = '"' .. name .. '"' end - return f("recipients %s don't exist; cannot send mail.", - table.concat(undeliverable_names, ", ") - ) + return false, f("recipients %s don't exist; cannot send mail.", table.concat(undeliverable_names, ", ")) end local extra = {} @@ -85,20 +65,26 @@ function mail.send(...) -- form the actual mail local msg = { - unread = true, - sender = m.from, - to = m.to, - cc = m.cc, + id = mail.new_uuid(), + from = m.from, + to = m.to, + cc = m.cc, + bcc = m.bcc, subject = m.subject, - body = m.body, - time = os.time(), + body = m.body, + time = os.time(), } - -- send the mail to all recipients + -- add in senders outbox + local entry = mail.get_storage_entry(m.from) + table.insert(entry.outbox, 1, msg) + mail.set_storage_entry(m.from, entry) + + -- add in every receivers inbox for recipient in pairs(recipients) do - local messages = mail.getMessages(recipient) - table.insert(messages, 1, msg) - mail.setMessages(recipient, messages) + entry = mail.get_storage_entry(recipient) + table.insert(entry.inbox, msg) + mail.set_storage_entry(recipient, entry) end -- notify recipients that happen to be online @@ -115,4 +101,6 @@ function mail.send(...) break end end + + return true end diff --git a/api.md b/api.md index b0800e2..7ef2ea9 100644 --- a/api.md +++ b/api.md @@ -9,47 +9,28 @@ mail = { cc = "carbon copy", bcc = "players, which, get, a, copy, but, are, not, visible, to, others", subject = "subject line", - body = "mail body", - -- 8 attachments max - attachments = {"default:stone 99", "default:gold_ingot 99"} + body = "mail body" } ``` -The fields `to`, `cc` and `bcc` can contain a player, multiple player names separated by commas, or be empty. Players in `to` are the recipiants, who are addressed directly. `cc` specifies players that get the mail to get notified, but are not immediate part of the conversation. There is no technical difference between `to` and `cc`, it just implies meaning for the players. Players can see all fields making up the mail except `bcc`, which is the only difference to `cc`. - -Attachments need to be provided for each player getting the mail. Until this is implemented, trying to send a mail to multiple players will fail. - -The `from` and `to` fields were renamed from the previous format: - -```lua -mail = { - src = "source name", - dst = "destination name", - subject = "subject line", - body = "mail body", - -- 8 attachments max - attachments = {"default:stone 99", "default:gold_ingot 99"} -} -``` +The fields `to`, `cc` and `bcc` can contain a player, multiple player names separated by commas, or be empty. +Players in `to` are the recipiants, who are addressed directly. `cc` specifies players that get the mail to get notified, but are not immediate part of the conversation. +There is no technical difference between `to` and `cc`, it just implies meaning for the players. +Players can see all fields making up the mail except `bcc`, which is the only difference to `cc`. ## Sending mail -Old variant (pre-1.1) -```lua -local error = mail.send("source name", "destination name", "subject line", "mail body") --- error will contain an error message if mail couldn't be delivered, otherwise nil -``` -New variant (1.1+) ```lua -local error = mail.send({ - from = "sender name", - to = "destination name", - cc = "carbon copy", - bcc = "blind carbon copy", +local success, error = mail.send({ + from = "singleplayer", + to = "playername", + cc = "carbon, copy", + bcc = "blind, carbon, copy", subject = "subject line", body = "mail body" }) --- error will contain an error message if mail couldn't be delivered, otherwise nil + +-- if "success" is false the error parameter will contain a message ``` # Hooks @@ -61,22 +42,56 @@ mail.register_on_receive(function(m) end) ``` -# internal mail format (on-disk) -The mail format on-disk +# Internals -> (worldfolder)/mails/(playername).json - -```json -[{ - "unread": true, - "sender": "sender name", - "subject": "subject name", - "body": "main\nmultiline\nbody", - "time": 1551258349, - "attachments": [ - "default:stone 99", - "default:gold_ingot 99" - ] -}] - -``` +mod-storage entry for a player (indexed by playername and serialized with json): +```lua +{ + contacts = { + { + -- name of the player (unique key in the list) + name = "", + -- note + note = "" + },{ + ... + } + }, + inbox = { + { + -- globally unique mail id + id = "d6cce35c-487a-458f-bab2-9032c2621f38", + -- sending player name + from = "", + -- receiving player name + to = "", + -- carbon copy (optional) + cc = "playername, playername2", + -- blind carbon copy (optional) + bcc = "", + -- mail subject + subject = "", + -- mail body + body = "", + -- timestamp (os.time()) + time = 1234, + -- read-flag (true: player has read the mail, inbox only) + read = true + },{ + ... + } + }, + outbox = { + -- same format as "inbox" + }, + lists = { + { + -- name of the maillist (unique key in the list) + name = "", + -- description + description = "", + -- playername list + players = {"playername", "playername2"} + } + } +} diff --git a/api.spec.lua b/api.spec.lua new file mode 100644 index 0000000..a9f9d2f --- /dev/null +++ b/api.spec.lua @@ -0,0 +1,12 @@ +mtt.register("send mail", function(callback) + -- send a mail + local success, err = mail.send({from = "player1", to = "player2", subject = "something", body = "blah"}) + assert(success) + assert(not err) + + -- check the receivers inbox + local entry = mail.get_storage_entry("player2") + assert(entry) + assert(#entry.inbox > 0) + callback() +end) diff --git a/gui.lua b/gui.lua index 07f0a2f..9607771 100644 --- a/gui.lua +++ b/gui.lua @@ -1,189 +1,8 @@ --- refactor these to some proper management thing -mail.selected_idxs = { - messages = {}, - contacts = {}, - to = {}, - cc = {}, - bcc = {}, -} - -mail.message_drafts = {} - -local selected_idxs = mail.selected_idxs -local message_drafts = mail.message_drafts - -local theme -if minetest.get_modpath("default") then - theme = default.gui_bg .. default.gui_bg_img -else - theme = "" -end - -mail.inbox_formspec = "size[8,9;]" .. theme .. [[ - button[6,0.10;2,0.5;new;New] - button[6,0.95;2,0.5;read;Read] - button[6,1.70;2,0.5;reply;Reply] - button[6,2.45;2,0.5;replyall;Reply All] - button[6,3.20;2,0.5;forward;Forward] - button[6,3.95;2,0.5;delete;Delete] - button[6,4.82;2,0.5;markread;Mark Read] - button[6,5.55;2,0.5;markunread;Mark Unread] - button[6,6.55;2,0.5;contacts;Contacts] - button[6,7.40;2,0.5;about;About] - button_exit[6,8.45;2,0.5;quit;Close] - - tablecolumns[color;text;text] - table[0,0;5.75,9;messages;#999,From,Subject]] - -mail.contacts_formspec = "size[8,9;]" .. theme .. [[ - button[6,0.10;2,0.5;new;New] - button[6,0.85;2,0.5;edit;Edit] - button[6,1.60;2,0.5;delete;Delete] - button[6,8.25;2,0.5;back;Back] - tablecolumns[color;text;text] - table[0,0;5.75,9;contacts;#999,Name,Note]] - -mail.select_contact_formspec = "size[8,9;]" .. theme .. [[ - tablecolumns[color;text;text] - table[0,0;3.5,9;contacts;#999,Name,Note%s] - button[3.55,2.00;1.75,0.5;toadd;→ Add] - button[3.55,2.75;1.75,0.5;toremove;← Remove] - button[3.55,6.00;1.75,0.5;ccadd;→ Add] - button[3.55,6.75;1.75,0.5;ccremove;← Remove] - tablecolumns[color;text;text] - table[5.15,0.0;2.75,4.5;to;#999,TO:,Note%s] - tablecolumns[color;text;text] - table[5.15,4.6;2.75,4.5;cc;#999,CC:,Note%s] - button[3.55,8.25;1.75,0.5;back;Back] - ]] - - -function mail.show_about(name) - local formspec = [[ - size[8,5;] - button[7.25,0;0.75,0.5;back;X] - label[0,0;Mail] - label[0,0.5;By cheapie] - label[0,1;http://github.com/cheapie/mail] - label[0,1.5;See LICENSE file for license information] - label[0,2.5;NOTE: Communication using this system] - label[0,3;is NOT guaranteed to be private!] - label[0,3.5;Admins are able to view the messages] - label[0,4;of any player.] - ]] .. theme - - minetest.show_formspec(name, "mail:about", formspec) -end - -function mail.show_inbox(name) - local formspec = { mail.inbox_formspec } - local messages = mail.getMessages(name) - - message_drafts[name] = nil - - if messages[1] then - for _, message in ipairs(messages) do - mail.ensure_new_format(message, name) - if message.unread then - if not mail.player_in_list(name, message.to) then - formspec[#formspec + 1] = ",#FFD788" - else - formspec[#formspec + 1] = ",#FFD700" - end - else - if not mail.player_in_list(name, message.to) then - formspec[#formspec + 1] = ",#CCCCDD" - else - formspec[#formspec + 1] = "," - end - end - formspec[#formspec + 1] = "," - formspec[#formspec + 1] = minetest.formspec_escape(message.sender) - formspec[#formspec + 1] = "," - if message.subject ~= "" then - if string.len(message.subject) > 30 then - formspec[#formspec + 1] = - minetest.formspec_escape(string.sub(message.subject, 1, 27)) - formspec[#formspec + 1] = "..." - else - formspec[#formspec + 1] = minetest.formspec_escape(message.subject) - end - else - formspec[#formspec + 1] = "(No subject)" - end - end - if selected_idxs.messages[name] then - formspec[#formspec + 1] = ";" - formspec[#formspec + 1] = tostring(selected_idxs.messages[name] + 1) - end - formspec[#formspec + 1] = "]" - else - formspec[#formspec + 1] = "]label[2.25,4.5;No mail]" - end - minetest.show_formspec(name, "mail:inbox", table.concat(formspec, "")) -end - -function mail.show_contacts(name) - local formspec = mail.contacts_formspec .. mail.compile_contact_list(name, selected_idxs.contacts[name]) - minetest.show_formspec(name, "mail:contacts", formspec) -end - -function mail.show_edit_contact(name, contact_name, note, illegal_name_hint) - local formspec = [[ - size[6,7] - button[4,6.25;2,0.5;back;Back] - field[0.25,0.5;4,1;name;Player name:;%s] - textarea[0.25,1.6;4,6.25;note;Note:;%s] - button[4,0.10;2,1;save;Save] - ]] - if illegal_name_hint == "collision" then - formspec = formspec .. [[ - label[4,1;That name] - label[4,1.5;is already in] - label[4,2;your contacts.] - ]] - elseif illegal_name_hint == "empty" then - formspec = formspec .. [[ - label[4,1;The contact] - label[4,1.5;name cannot] - label[4,2;be empty.] - ]] - end - formspec = formspec .. theme - formspec = string.format(formspec, - minetest.formspec_escape(contact_name or ""), - minetest.formspec_escape(note or "")) - minetest.show_formspec(name, "mail:editcontact", formspec) -end - -function mail.show_select_contact(name, to, cc) - local formspec = mail.select_contact_formspec - local contacts = mail.compile_contact_list(name, selected_idxs.contacts[name]) - - -- compile lists - if to then - to = mail.compile_contact_list(name, selected_idxs.to[name], to) - else - to = "" - end - if cc then - cc = mail.compile_contact_list(name, selected_idxs.cc[name], cc) - else - cc = "" - end - --[[if bcc then - bcc = table.concat(mail.compile_contact_list(name, selected_idxs.bcc[name], bcc) - else - bcc = "" - end]]-- - formspec = string.format(formspec, contacts, to, cc)--, bcc() - minetest.show_formspec(name, "mail:selectcontact", formspec) -end function mail.compile_contact_list(name, selected, playernames) -- TODO: refactor this - not just compiles *a* list, but *the* list for the contacts screen (too inflexible) local formspec = {} - local contacts = mail.getContacts(name) + local contacts = mail.get_contacts(name) if playernames == nil then local length = 0 @@ -250,465 +69,9 @@ function mail.compile_contact_list(name, selected, playernames) formspec[#formspec + 1] = "]" end return table.concat(formspec, "") + end -function mail.show_message(name, msgnumber) - local messages = mail.getMessages(name) - local message = messages[msgnumber] - local formspec = [[ - size[8,9] - - box[0,0;7,1.9;#466432] - - button[7.25,0.15;0.75,0.5;back;X] - - label[0.2,0.1;From: %s] - label[0.2,0.5;To: %s] - label[0.2,0.9;CC: %s] - label[0.2,1.3;Date: %s] - - label[0,2.1;Subject: %s] - textarea[0.25,2.6;8,7.0;;;%s] - - button[0,8.5;2,1;reply;Reply] - button[2,8.5;2,1;replyall;Reply All] - button[4,8.5;2,1;forward;Forward] - button[6,8.5;2,1;delete;Delete] - ]] .. theme - - local from = minetest.formspec_escape(message.sender) or "" - local to = minetest.formspec_escape(message.to) or "" - local cc = minetest.formspec_escape(message.cc) or "" - local date = type(message.time) == "number" - and minetest.formspec_escape(os.date("%Y-%m-%d %X", message.time)) or "" - local subject = minetest.formspec_escape(message.subject) or "" - local body = minetest.formspec_escape(message.body) or "" - formspec = string.format(formspec, from, to, cc, date, subject, body) - - if message.unread then - message.unread = false - mail.setMessages(name, messages) - end - - minetest.show_formspec(name,"mail:message",formspec) -end - -function mail.show_compose(name, defaultto, defaultsubj, defaultbody, defaultcc, defaultbcc) - local formspec = [[ - size[8,9] - button[0,0;1,1;tocontacts;To:] - field[1.1,0.3;3.2,1;to;;%s] - button[4,0;1,1;cccontacts;CC:] - field[5.1,0.3;3.1,1;cc;;%s] - button[4,0.75;1,1;bcccontacts;BCC:] - field[5.1,1.05;3.1,1;bcc;;%s] - field[0.25,2;8,1;subject;Subject:;%s] - textarea[0.25,2.5;8,6;body;;%s] - button[0.5,8.5;3,1;cancel;Cancel] - button[4.5,8.5;3,1;send;Send] - ]] .. theme - - defaultto = defaultto or "" - defaultsubj = defaultsubj or "" - defaultbody = defaultbody or "" - defaultcc = defaultcc or "" - defaultbcc = defaultbcc or "" - - formspec = string.format(formspec, - minetest.formspec_escape(defaultto), - minetest.formspec_escape(defaultcc), - minetest.formspec_escape(defaultbcc), - minetest.formspec_escape(defaultsubj), - minetest.formspec_escape(defaultbody)) - - minetest.show_formspec(name, "mail:compose", formspec) -end - -function mail.reply(name, message) - mail.ensure_new_format(message) - local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body - mail.show_compose(name, message.sender, "Re: "..message.subject, replyfooter) -end - -function mail.replyall(name, message) - mail.ensure_new_format(message) - local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body - - -- new recipients are the sender plus the original recipients, minus ourselves - local recipients = message.to or "" - if message.sender ~= nil then - recipients = message.sender .. ", " .. recipients - end - recipients = mail.parse_player_list(recipients) - for k,v in pairs(recipients) do - if v == name then - table.remove(recipients, k) - break - end - end - recipients = mail.concat_player_list(recipients) - - -- new CC is old CC minus ourselves - local cc = mail.parse_player_list(message.cc) - for k,v in pairs(cc) do - if v == name then - table.remove(cc, k) - break - end - end - cc = mail.concat_player_list(cc) - - mail.show_compose(name, recipients, "Re: "..message.subject, replyfooter, cc) -end - -function mail.forward(name, message) - local fwfooter = "Type your message here.\n\n--Original message follows--\n" .. (message.body or "") - mail.show_compose(name, "", "Fw: " .. (message.subject or ""), fwfooter) -end - -function mail.handle_receivefields(player, formname, fields) - if formname == "mail:about" then - minetest.after(0.5, function() - mail.show_inbox(player:get_player_name()) - end) - return true - - elseif formname == "mail:inbox" then - local name = player:get_player_name() - local messages = mail.getMessages(name) - - if fields.messages then - local evt = minetest.explode_table_event(fields.messages) - selected_idxs.messages[name] = evt.row - 1 - if evt.type == "DCL" and messages[selected_idxs.messages[name]] then - mail.show_message(name, selected_idxs.messages[name]) - end - return true - end - - if fields.read then - if messages[selected_idxs.messages[name]] then - mail.show_message(name, selected_idxs.messages[name]) - end - - elseif fields.delete then - if messages[selected_idxs.messages[name]] then - table.remove(messages, selected_idxs.messages[name]) - mail.setMessages(name, messages) - end - - mail.show_inbox(name) - - elseif fields.reply and messages[selected_idxs.messages[name]] then - local message = messages[selected_idxs.messages[name]] - mail.reply(name, message) - - elseif fields.replyall and messages[selected_idxs.messages[name]] then - local message = messages[selected_idxs.messages[name]] - mail.replyall(name, message) - - elseif fields.forward and messages[selected_idxs.messages[name]] then - local message = messages[selected_idxs.messages[name]] - mail.forward(name, message) - - elseif fields.markread then - if messages[selected_idxs.messages[name]] then - messages[selected_idxs.messages[name]].unread = false - -- set messages immediately, so it shows up already when updating the inbox - mail.setMessages(name, messages) - end - mail.show_inbox(name) - - elseif fields.markunread then - if messages[selected_idxs.messages[name]] then - messages[selected_idxs.messages[name]].unread = true - -- set messages immediately, so it shows up already when updating the inbox - mail.setMessages(name, messages) - end - mail.show_inbox(name) - - elseif fields.new then - mail.show_compose(name) - - elseif fields.contacts then - mail.show_contacts(name) - - elseif fields.about then - mail.show_about(name) - - end - - return true - - elseif formname == "mail:message" then - local name = player:get_player_name() - local messages = mail.getMessages(name) - - if fields.back then - mail.show_inbox(name) - return true -- don't uselessly set messages - - elseif fields.reply then - local message = messages[selected_idxs.messages[name]] - mail.reply(name, message) - - elseif fields.replyall then - local message = messages[selected_idxs.messages[name]] - mail.replyall(name, message) - - elseif fields.forward then - local message = messages[selected_idxs.messages[name]] - mail.forward(name, message) - - elseif fields.delete then - if messages[selected_idxs.messages[name]] then - table.remove(messages,selected_idxs.messages[name]) - mail.setMessages(name, messages) - end - mail.show_inbox(name) - end - - return true - - elseif formname == "mail:compose" then - local name = player:get_player_name() - if fields.send then - local error = mail.send({ - from = name, - to = fields.to, - cc = fields.cc, - bcc = fields.bcc, - subject = fields.subject, - body = fields.body, - }) - if error then - minetest.chat_send_player(name, error) - return - end - - local contacts = mail.getContacts(name) - local recipients = mail.parse_player_list(fields.to) - local changed = false - for _,v in pairs(recipients) do - if contacts[string.lower(v)] == nil then - contacts[string.lower(v)] = { - name = v, - note = "", - } - changed = true - end - end - if changed then - mail.setContacts(name, contacts) - end - - minetest.after(0.5, function() - mail.show_inbox(name) - end) - - elseif fields.tocontacts or fields.cccontacts or fields.bcccontacts then - message_drafts[name] = { - to = fields.to, - cc = fields.cc, - bcc = fields.bcc, - subject = fields.subject, - body = fields.body, - } - mail.show_select_contact(name, fields.to, fields.cc, fields.bcc) - - elseif fields.cancel then - message_drafts[name] = nil - mail.show_inbox(name) - end - - return true - - elseif formname == "mail:selectcontact" then - local name = player:get_player_name() - local contacts = mail.getContacts(name) - local draft = message_drafts[name] - - -- get indexes for fields with selected rows - -- execute their default button's actions if double clicked - for k,action in pairs({ - contacts = "toadd", - to = "toremove", - cc = "ccremove", - bcc = "bccremove" - }) do - if fields[k] then - local evt = minetest.explode_table_event(fields[k]) - selected_idxs[k][name] = evt.row - 1 - if evt.type == "DCL" and selected_idxs[k][name] then - fields[action] = true - end - return true - end - end - - local update = false - -- add - for _,v in pairs({"to","cc","bcc"}) do - if fields[v.."add"] then - update = true - if selected_idxs.contacts[name] then - for k, contact, i in mail.pairsByKeys(contacts) do - if k == selected_idxs.contacts[name] or i == selected_idxs.contacts[name] then - local list = mail.parse_player_list(draft[v]) - list[#list+1] = contact.name - selected_idxs[v][name] = #list - draft[v] = mail.concat_player_list(list) - break - end - end - end - end - end - -- remove - for _,v in pairs({"to","cc","bcc"}) do - if fields[v.."remove"] then - update = true - if selected_idxs[v][name] then - local list = mail.parse_player_list(draft[v]) - table.remove(list, selected_idxs[v][name]) - if #list < selected_idxs[v][name] then - selected_idxs[v][name] = #list - end - draft[v] = mail.concat_player_list(list) - end - end - end - - if update then - mail.show_select_contact(name, draft.to, draft.cc, draft.bcc) - return true - end - - -- delete old idxs - for _,v in ipairs({"contacts","to","cc","bcc"}) do - selected_idxs[v][name] = nil - end - - mail.show_compose(name, draft.to, draft.subject, draft.body, draft.cc, draft.bcc) - - return true - - elseif formname == "mail:contacts" then - local name = player:get_player_name() - local contacts = mail.getContacts(name) - - if fields.contacts then - local evt = minetest.explode_table_event(fields.contacts) - for k, _, i in mail.pairsByKeys(contacts) do - if i == evt.row - 1 then - selected_idxs.contacts[name] = k - break - end - end - if evt.type == "DCL" and contacts[selected_idxs.contacts[name]] then - mail.show_edit_contact( - name, - contacts[selected_idxs.contacts[name]].name, - contacts[selected_idxs.contacts[name]].note - ) - end - - elseif fields.new then - selected_idxs.contacts[name] = "#NEW#" - mail.show_edit_contact(name, "", "") - - elseif fields.edit and selected_idxs.contacts[name] and contacts[selected_idxs.contacts[name]] then - mail.show_edit_contact( - name, - contacts[selected_idxs.contacts[name]].name, - contacts[selected_idxs.contacts[name]].note - ) - - elseif fields.delete then - if contacts[selected_idxs.contacts[name]] then - -- delete the contact and set the selected to the next in the list, - -- except if it was the last. Then determine the new last - local found = false - local last = nil - for k in mail.pairsByKeys(contacts) do - if found then - selected_idxs.contacts[name] = k - break - elseif k == selected_idxs.contacts[name] then - contacts[selected_idxs.contacts[name]] = nil - selected_idxs.contacts[name] = nil - found = true - else - last = k - end - end - if found and not selected_idxs.contacts[name] then - -- was the last in the list, so take the previous (new last) - selected_idxs.contacts[name] = last - end - - mail.setContacts(name, contacts) - end - - mail.show_contacts(name) - - elseif fields.back then - mail.show_inbox(name) - end - - return true - - elseif formname == "mail:editcontact" then - local name = player:get_player_name() - local contacts = mail.getContacts(name) - - if fields.save then - if selected_idxs.contacts[name] and selected_idxs.contacts[name] ~= "#NEW#" then - local contact = contacts[selected_idxs.contacts[name]] - if selected_idxs.contacts[name] ~= string.lower(fields.name) then - -- name changed! - if #fields.name == 0 then - mail.show_edit_contact(name, contact.name, fields.note, "empty") - return true - - elseif contacts[string.lower(fields.name)] ~= nil then - mail.show_edit_contact(name, contact.name, fields.note, "collision") - return true - - else - contacts[string.lower(fields.name)] = contact - contacts[selected_idxs.contacts[name]] = nil - end - end - contact.name = fields.name - contact.note = fields.note - - else - local contact = { - name = fields.name, - note = fields.note, - } - contacts[string.lower(contact.name)] = contact - end - - mail.setContacts(name, contacts) - mail.show_contacts(name) - - elseif fields.back then - mail.show_contacts(name) - end - - return true - - elseif fields.mail then - mail.show_inbox(player:get_player_name()) - return true - end -end - -minetest.register_on_player_receive_fields(mail.handle_receivefields) - - if minetest.get_modpath("unified_inventory") then mail.receive_mail_message = mail.receive_mail_message .. " or use the mail button in the inventory" @@ -718,6 +81,9 @@ if minetest.get_modpath("unified_inventory") then unified_inventory.register_button("mail", { type = "image", image = "mail_button.png", - tooltip = "Mail" + tooltip = "Mail", + action = function(player) + mail.show_mail_menu(player:get_player_name()) + end }) end diff --git a/init.lua b/init.lua index c607b1b..43ffd1b 100644 --- a/init.lua +++ b/init.lua @@ -1,15 +1,38 @@ mail = { - -- api version - apiversion = 1.1, + -- version + version = 3, - -- mail directory - maildir = minetest.get_worldpath().."/mails", - contactsdir = minetest.get_worldpath().."/mails/contacts" + -- mod storage + storage = minetest.get_mod_storage(), + + -- ui theme prepend + theme = "", + + -- ui forms + ui = {}, + + -- per-user ephemeral data + selected_idxs = { + inbox = {}, + sent = {}, + contacts = {}, + maillists = {}, + to = {}, + cc = {}, + bcc = {}, + boxtab = {} + }, + + message_drafts = {} } +if minetest.get_modpath("default") then + mail.theme = default.gui_bg .. default.gui_bg_img +end local MP = minetest.get_modpath(minetest.get_current_modname()) dofile(MP .. "/util/normalize.lua") +dofile(MP .. "/util/uuid.lua") dofile(MP .. "/chatcommands.lua") dofile(MP .. "/migrate.lua") dofile(MP .. "/hud.lua") @@ -17,10 +40,26 @@ dofile(MP .. "/storage.lua") dofile(MP .. "/api.lua") dofile(MP .. "/gui.lua") dofile(MP .. "/onjoin.lua") +dofile(MP .. "/ui/mail.lua") +dofile(MP .. "/ui/inbox.lua") +dofile(MP .. "/ui/outbox.lua") +dofile(MP .. "/ui/message.lua") +dofile(MP .. "/ui/events.lua") +dofile(MP .. "/ui/contacts.lua") +dofile(MP .. "/ui/edit_contact.lua") +dofile(MP .. "/ui/select_contact.lua") +dofile(MP .. "/ui/maillists.lua") +dofile(MP .. "/ui/edit_maillists.lua") +dofile(MP .. "/ui/compose.lua") +dofile(MP .. "/ui/about.lua") -- migrate storage mail.migrate() if minetest.get_modpath("mtt") then dofile(MP .. "/mtt.lua") -end \ No newline at end of file + dofile(MP .. "/api.spec.lua") + dofile(MP .. "/migrate.spec.lua") + dofile(MP .. "/util/uuid.spec.lua") + dofile(MP .. "/util/normalize.spec.lua") +end diff --git a/migrate.lua b/migrate.lua index 1cefbd7..5378686 100644 --- a/migrate.lua +++ b/migrate.lua @@ -1,50 +1,98 @@ --- migrate from mail.db to player-file-based mailbox +local STORAGE_VERSION_KEY = "@@version" + +local function migrate_v1_to_v3() + local file = io.open(minetest.get_worldpath().."/mail.db", "r") + assert(file) + print("[mail] Migration from v1 to v3 database") + + local data = file:read("*a") + local oldmails = minetest.deserialize(data) + file:close() + + for name, oldmessages in pairs(oldmails) do + print("[mail,v1] + migrating player '" .. name .. "'") + local entry = mail.get_storage_entry(name) + for _, msg in ipairs(oldmessages) do + table.insert(entry.inbox, { + id = mail.new_uuid(), + from = msg.sender or msg.from, + to = msg.to or name, + subject = msg.subject, + body = msg.body, + time = msg.time or os.time(), + }) + end + mail.set_storage_entry(name, entry) + end + + -- rename file + print("[mail,v1] migration done, renaming old mail.db") + os.rename(minetest.get_worldpath().."/mail.db", minetest.get_worldpath().."/mail.db.old") +end + +local function read_json_file(path) + local file = io.open(path, "r") + local content = {} + if file then + local json = file:read("*a") + content = minetest.parse_json(json or "[]") or {} + file:close() + end + return content +end + +-- migrate from v2 to v3 database +local function migrate_v2_to_v3() + local maildir = minetest.get_worldpath().."/mails" + minetest.mkdir(maildir) -- if necessary (eg. first login) + print("[mail] Migration from v2 to v3 database") + + -- defer execution until auth-handler ready (first server-step) + minetest.after(0, function() + for playername, _ in minetest.get_auth_handler().iterate() do + local entry = mail.get_storage_entry(playername) + + local player_contacts = read_json_file(maildir .. "/contacts/" .. playername .. ".json") + for _, c in pairs(player_contacts) do + table.insert(entry.contacts, { name = c.name, note = c.note }) + end + + local saneplayername = string.gsub(playername, "[.|/]", "") + local player_inbox = read_json_file(maildir .. "/" .. saneplayername .. ".json") + print("[mail,v2] + migrating player '" .. playername .. "'") + for _, msg in ipairs(player_inbox) do + table.insert(entry.inbox, { + id = mail.new_uuid(), + from = msg.sender or msg.from, + to = msg.to or playername, + cc = msg.cc, + subject = msg.subject, + body = msg.body, + time = msg.time or os.time(), + }) + end + + mail.set_storage_entry(playername, entry) + end + print("[mail,v2] migration done") + end) +end function mail.migrate() - -- create directory, just in case - minetest.mkdir(mail.maildir) - minetest.mkdir(mail.contactsdir) - - local file = io.open(minetest.get_worldpath().."/mail.db", "r") - if file then - print("[mail] migrating to new per-player storage") - - local data = file:read("*a") - local oldmails = minetest.deserialize(data) - file:close() - - for name, oldmessages in pairs(oldmails) do - mail.setMessages(name, oldmessages) - end - - -- rename file - print("[mail] migration done, renaming old mail.db") - os.rename(minetest.get_worldpath().."/mail.db", minetest.get_worldpath().."/mail.db.old") + -- check for v2 storage first, v1-migration might have set the v3-flag already + local version = mail.storage:get_int(STORAGE_VERSION_KEY) + if version < 3 then + -- v2 to v3 + migrate_v2_to_v3() + mail.storage:set_int(STORAGE_VERSION_KEY, 3) end -end - - -function mail.migrate_contacts(playername) - local file = io.open(mail.getContactsFile(playername), 'r') - if not file then - -- file doesn't exist! This is a case for Migrate Man! - local messages = mail.getMessages(playername) - local contacts = {} - - if messages and not contacts then - for _, message in pairs(messages) do - mail.ensure_new_format(message) - if contacts[string.lower(message.from)] == nil then - contacts[string.lower(message.from)] = { - name = message.from, - note = "", - } - end - end - end - else - file:close() -- uh, um, nope, let's leave those alone, shall we? + -- check for v1 storage + local v1_file = io.open(minetest.get_worldpath().."/mail.db", "r") + if v1_file then + -- v1 to v3 + migrate_v1_to_v3() + mail.storage:set_int(STORAGE_VERSION_KEY, 3) end -end +end \ No newline at end of file diff --git a/migrate.spec.lua b/migrate.spec.lua new file mode 100644 index 0000000..b98341b --- /dev/null +++ b/migrate.spec.lua @@ -0,0 +1,28 @@ + +mtt.register("migrate v1", function(callback) + local entry = mail.get_storage_entry("old_v1_player") + assert(entry) + assert(#entry.inbox == 1) + assert(entry.inbox[1].from == "singleplayer") + assert(entry.inbox[1].to == "old_v1_player") + assert(entry.inbox[1].subject == "test1") + assert(entry.inbox[1].body == "test2") + assert(entry.inbox[1].id) + assert(entry.inbox[1].time > 0) + + callback() +end) + +mtt.register("migrate v2", function(callback) + local entry = mail.get_storage_entry("old_v2_player") + assert(entry) + assert(#entry.inbox == 1) + assert(entry.inbox[1].from == "someone-else") + assert(entry.inbox[1].to == "old_v2_player") + assert(entry.inbox[1].subject == "test1") + assert(entry.inbox[1].body == "test2") + assert(entry.inbox[1].id) + assert(entry.inbox[1].time == 1678467148) + + callback() +end) \ No newline at end of file diff --git a/mtt.lua b/mtt.lua index 9b12fb7..7c8cf0b 100644 --- a/mtt.lua +++ b/mtt.lua @@ -1,14 +1,10 @@ -mtt.register("send mail", function(callback) - -- create "player2" +mtt.register("setup", function(callback) + -- create test players local auth_handler = minetest.get_auth_handler() + auth_handler.set_password("player1", "") auth_handler.set_password("player2", "") + auth_handler.set_password("player3", "") - -- send a mail - mail.send("player1", "player2", "something", "blah") - - -- check the receivers inbox - local list2 = mail.getMessages("player2") - assert(list2 ~= nil and #list2 > 0) callback() end) \ No newline at end of file diff --git a/onjoin.lua b/onjoin.lua index c8e1877..0bf36d2 100644 --- a/onjoin.lua +++ b/onjoin.lua @@ -1,6 +1,7 @@ minetest.register_on_joinplayer(function(player) minetest.after(2, function(name) - local messages = mail.getMessages(name) + local entry = mail.get_storage_entry(name) + local messages = entry.inbox local unreadcount = 0 @@ -16,6 +17,4 @@ minetest.register_on_joinplayer(function(player) end end, player:get_player_name()) - - mail.migrate_contacts(player:get_player_name()) end) diff --git a/storage.lua b/storage.lua index 8e028eb..89a1eab 100644 --- a/storage.lua +++ b/storage.lua @@ -1,47 +1,195 @@ +-- storage getter/setter +local STORAGE_PREFIX = "mail/" -function mail.getMailFile(playername) - local saneplayername = string.gsub(playername, "[.|/]", "") - return mail.maildir .. "/" .. saneplayername .. ".json" +-- create or populate empty fields on an entry +local function populate_entry(e) + e = e or {} + e.contacts = e.contacts or {} + e.inbox = e.inbox or {} + e.outbox = e.outbox or {} + e.lists = e.lists or {} + return e end -function mail.getContactsFile(playername) - local saneplayername = string.gsub(playername, "[.|/]", "") - return mail.maildir .. "/contacts/" .. saneplayername .. ".json" -end - - -function mail.getMessages(playername) - local messages = mail.read_json_file(mail.getMailFile(playername)) - if messages then - for _, msg in ipairs(messages) do - if not msg.time then - -- add missing time field if not available (happens with old data) - msg.time = 0 - end - end - - -- sort by received date descending - table.sort(messages, function(a,b) return a.time > b.time end) - -- show hud notification - mail.hud_update(playername, messages) - end - - return messages -end - -function mail.setMessages(playername, messages) - if mail.write_json_file(mail.getMailFile(playername), messages) then - mail.hud_update(playername, messages) - return true +function mail.get_storage_entry(playername) + local str = mail.storage:get_string(STORAGE_PREFIX .. playername) + if str == "" then + -- new entry + return populate_entry() else - minetest.log("error","[mail] Save failed - messages may be lost! ("..playername..")") - return false + -- deserialize existing entry + local e = minetest.parse_json(str) + return populate_entry(e) end end +function mail.set_storage_entry(playername, entry) + mail.storage:set_string(STORAGE_PREFIX .. playername, minetest.write_json(entry)) +end -function mail.getContacts(playername) - return mail.read_json_file(mail.getContactsFile(playername)) +-- get a mail by id from the players in- or outbox +function mail.get_message(playername, msg_id) + local entry = mail.get_storage_entry(playername) + for _, msg in ipairs(entry.inbox) do + if msg.id == msg_id then + return msg + end + end + for _, msg in ipairs(entry.outbox) do + if msg.id == msg_id then + return msg + end + end +end + +-- marks a mail read by its id +function mail.mark_read(playername, msg_id) + local entry = mail.get_storage_entry(playername) + for _, msg in ipairs(entry.inbox) do + if msg.id == msg_id then + msg.read = true + mail.set_storage_entry(playername, entry) + return + end + end +end + +-- marks a mail unread by its id +function mail.mark_unread(playername, msg_id) + local entry = mail.get_storage_entry(playername) + for _, msg in ipairs(entry.inbox) do + if msg.id == msg_id then + msg.read = false + mail.set_storage_entry(playername, entry) + return + end + end +end + +-- deletes a mail by its id +function mail.delete_mail(playername, msg_id) + local entry = mail.get_storage_entry(playername) + for i, msg in ipairs(entry.inbox) do + if msg.id == msg_id then + table.remove(entry.outbox, i) + mail.set_storage_entry(playername, entry) + return + end + end + for i, msg in ipairs(entry.outbox) do + if msg.id == msg_id then + table.remove(entry.outbox, i) + mail.set_storage_entry(playername, entry) + return + end + end +end + +-- add or update a contact +function mail.update_contact(playername, contact) + local entry = mail.get_storage_entry(playername) + local existing_updated = false + for i, existing_contact in ipairs(entry.contacts) do + if existing_contact.name == contact.name then + -- update + entry.contacts[i] = contact + existing_updated = true + break + end + end + if not existing_updated then + -- insert + table.insert(entry.contacts, contact) + end + mail.set_storage_entry(playername, entry) +end + +-- deletes a contact +function mail.delete_contact(playername, contactname) + local entry = mail.get_storage_entry(playername) + for i, existing_contact in ipairs(entry.contacts) do + if existing_contact.name == contactname then + -- delete + table.remove(entry.contacts, i) + mail.set_storage_entry(playername, entry) + return + end + end +end + +-- get all contacts +function mail.get_contacts(playername) + local entry = mail.get_storage_entry(playername) + return entry.contacts +end + +-- returns the maillists of a player +function mail.get_maillists(playername) + local entry = mail.get_storage_entry(playername) + return entry.lists +end + +-- returns the maillists of a player +function mail.get_maillist_by_name(playername, listname) + local entry = mail.get_storage_entry(playername) + for _, list in ipairs(entry.lists) do + if list.name == listname then + return list + end + end +end + +-- updates or creates a maillist +function mail.update_maillist(playername, list) + local entry = mail.get_storage_entry(playername) + local existing_updated = false + for i, existing_list in ipairs(entry.lists) do + if existing_list.name == list.name then + -- update + entry.lists[i] = list + existing_updated = true + break + end + end + if not existing_updated then + -- insert + table.insert(entry.lists, list) + end + mail.set_storage_entry(playername, entry) +end + +function mail.delete_maillist(playername, listname) + local entry = mail.get_storage_entry(playername) + for i, list in ipairs(entry.lists) do + if list.name == listname then + -- delete + table.remove(entry.lists, i) + mail.set_storage_entry(playername, entry) + return + end + end +end + +function mail.extractMaillists(receivers_string, maillists_owner) + local globalReceivers = mail.parse_player_list(receivers_string) -- receivers including maillists + local receivers = {} -- extracted receivers + + -- extract players from mailing lists + for _, receiver in ipairs(globalReceivers) do + local receiverInfo = receiver:split("@") -- @maillist + if receiverInfo[1] and receiver == "@" .. receiverInfo[1] then + local maillist = mail.get_maillist_by_name(maillists_owner, receiverInfo[1]) + if maillist then + for _, playername in ipairs(maillist.players) do + table.insert(receivers, playername) + end + end + else -- in case of player + table.insert(receivers, receiver) + end + end + + return receivers end function mail.pairsByKeys(t, f) @@ -63,33 +211,3 @@ function mail.pairsByKeys(t, f) return iter end -function mail.setContacts(playername, contacts) - if mail.write_json_file(mail.getContactsFile(playername), contacts) then - return true - else - minetest.log("error","[mail] Save failed - contacts may be lost! ("..playername..")") - return false - end -end - - -function mail.read_json_file(path) - local file = io.open(path, "r") - local content = {} - if file then - local json = file:read("*a") - content = minetest.parse_json(json or "[]") or {} - file:close() - end - return content -end - -function mail.write_json_file(path, content) - local file = io.open(path,"w") - local json = minetest.write_json(content) - if file and file:write(json) and file:close() then - return true - else - return false - end -end diff --git a/storage.spec.lua b/storage.spec.lua new file mode 100644 index 0000000..1be9ea4 --- /dev/null +++ b/storage.spec.lua @@ -0,0 +1,45 @@ + +mtt.register("storage", function(callback) + -- sanity checks + local playername = "player1" + local entry = mail.get_storage_entry(playername) + assert(entry) + + -- create + local contact = { + name = "other-player", + note = "my-note" + } + mail.update_contact(playername, contact) + + -- read + local contacts = mail.get_contacts(playername) + assert(#contacts == 1) + assert(contacts[1].note == contact.note) + assert(contacts[1].name == contact.name) + + -- read through api + local contacts2 = mail.get_contacts(playername) + assert(#contacts2 == 1) + assert(contacts2[1].note == contact.note) + assert(contacts2[1].name == contact.name) + + -- update + mail.update_contact(playername, { + name = contact.name, + note = "xy" + }) + + -- read updated + contacts = mail.get_contacts(playername) + assert(#contacts == 1) + assert(contacts[1].note == "xy") + assert(contacts[1].name == contact.name) + + -- delete + mail.delete_contact(playername, contact.name) + contacts = mail.get_contacts(playername) + assert(#contacts == 0) + + callback() +end) \ No newline at end of file diff --git a/test/Dockerfile b/test/Dockerfile index 6324d80..9c1bd1b 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -1,6 +1,13 @@ ARG ENGINE_VERSION=5.5.0 FROM registry.gitlab.com/minetest/minetest/server:${ENGINE_VERSION} +# copy old v1 maildb for migration testing +COPY ./mail.db /root/.minetest/worlds/world/mail.db +# copy old v2 mail-dir and auth.sqlite for migration testing +COPY ./old_v2_player.json /root/.minetest/worlds/world/mails/ +COPY ./auth.sqlite /root/.minetest/worlds/world/auth.sqlite + + USER root RUN apk add git &&\ mkdir -p /root/.minetest/worlds/world/worldmods/ &&\ diff --git a/test/auth.sqlite b/test/auth.sqlite new file mode 100644 index 0000000..e6bac67 Binary files /dev/null and b/test/auth.sqlite differ diff --git a/test/mail.db b/test/mail.db new file mode 100644 index 0000000..3104acb --- /dev/null +++ b/test/mail.db @@ -0,0 +1 @@ +local _={};_[1]="singleplayer";return {old_v1_player={{unread=true,subject="test1",sender=_[1],body="test2"}},[_[1]]={}} \ No newline at end of file diff --git a/test/minetest.conf b/test/minetest.conf index 992f787..afea5f2 100644 --- a/test/minetest.conf +++ b/test/minetest.conf @@ -1,3 +1,4 @@ default_game = minetest_game mg_name = v7 -mtt_enable = true \ No newline at end of file +mtt_enable = true +mtt_filter = mail \ No newline at end of file diff --git a/test/old_v2_player.json b/test/old_v2_player.json new file mode 100644 index 0000000..2bb859a --- /dev/null +++ b/test/old_v2_player.json @@ -0,0 +1 @@ +[{"body":"test2","sender":"someone-else","subject":"test1","time":1678467148,"unread":false}] \ No newline at end of file diff --git a/ui/about.lua b/ui/about.lua new file mode 100644 index 0000000..36226fb --- /dev/null +++ b/ui/about.lua @@ -0,0 +1,27 @@ +local FORMNAME = "mail:about" + +function mail.show_about(name) + local formspec = [[ + size[8,5;] + button[7.25,0;0.75,0.5;back;X] + label[0,0;Mail] + label[0,0.5;By cheapie] + label[0,1;http://github.com/cheapie/mail] + label[0,1.5;See LICENSE file for license information] + label[0,2.5;NOTE: Communication using this system] + label[0,3;is NOT guaranteed to be private!] + label[0,3.5;Admins are able to view the messages] + label[0,4;of any player.] + ]] .. mail.theme + + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname) + if formname ~= FORMNAME then + return + end + + local playername = player:get_player_name() + mail.show_mail_menu(playername) +end) \ No newline at end of file diff --git a/ui/compose.lua b/ui/compose.lua new file mode 100644 index 0000000..ad7f653 --- /dev/null +++ b/ui/compose.lua @@ -0,0 +1,89 @@ +local FORMNAME = "mail:compose" + +function mail.show_compose(name, to, subject, body, cc, bcc) + local formspec = [[ + size[8,9] + button[0,0;1,1;tocontacts;To:] + field[1.1,0.3;3.2,1;to;;%s] + button[4,0;1,1;cccontacts;CC:] + field[5.1,0.3;3.1,1;cc;;%s] + button[4,0.75;1,1;bcccontacts;BCC:] + field[5.1,1.05;3.1,1;bcc;;%s] + field[0.25,2;8,1;subject;Subject:;%s] + textarea[0.25,2.5;8,6;body;;%s] + button[0.5,8.5;3,1;cancel;Cancel] + button[4.5,8.5;3,1;send;Send] + ]] .. mail.theme + + formspec = string.format(formspec, + minetest.formspec_escape(to or ""), + minetest.formspec_escape(cc or ""), + minetest.formspec_escape(bcc or ""), + minetest.formspec_escape(subject or ""), + minetest.formspec_escape(body or "")) + + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + if fields.send then + local success, err = mail.send({ + from = name, + to = fields.to, + cc = fields.cc, + bcc = fields.bcc, + subject = fields.subject, + body = fields.body, + }) + if not success then + minetest.chat_send_player(name, err) + return + end + + -- add new contacts if some receivers aren't registered + local contacts = mail.get_contacts(name) + local recipients = mail.parse_player_list(fields.to) + local isNew = true + for _,recipient in ipairs(recipients) do + if recipient:sub(1,1) == "@" then -- in case of maillist -- check if first char is @ + isNew = false + else + for _,contact in ipairs(contacts) do + if contact.name == recipient then + isNew = false + break + end + end + end + if isNew then + mail.update_contact(name, {name = recipient, note = ""}) + end + end + + minetest.after(0.5, function() + mail.show_mail_menu(name) + end) + + elseif fields.tocontacts or fields.cccontacts or fields.bcccontacts then + mail.message_drafts[name] = { + to = fields.to, + cc = fields.cc, + bcc = fields.bcc, + subject = fields.subject, + body = fields.body, + } + mail.show_select_contact(name, fields.to, fields.cc, fields.bcc) + + elseif fields.cancel then + mail.message_drafts[name] = nil + + mail.show_mail_menu(name) + end + + return true +end) \ No newline at end of file diff --git a/ui/contacts.lua b/ui/contacts.lua new file mode 100644 index 0000000..1ea642a --- /dev/null +++ b/ui/contacts.lua @@ -0,0 +1,83 @@ +local FORMNAME = "mail:contacts" + +local contacts_formspec = "size[8,9;]" .. mail.theme .. [[ + button[6,0.10;2,0.5;new;New] + button[6,0.85;2,0.5;edit;Edit] + button[6,1.60;2,0.5;delete;Delete] + button[6,8.25;2,0.5;back;Back] + tablecolumns[color;text;text] + table[0,0;5.75,9;contacts;#999,Name,Note]] + + +function mail.show_contacts(name) + local formspec = contacts_formspec .. mail.compile_contact_list(name, mail.selected_idxs.contacts[name]) + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local contacts = mail.get_contacts(name) + + if fields.contacts then + local evt = minetest.explode_table_event(fields.contacts) + for k, _, i in mail.pairsByKeys(contacts) do + if i == evt.row - 1 then + mail.selected_idxs.contacts[name] = k + break + end + end + if evt.type == "DCL" and contacts[mail.selected_idxs.contacts[name]] then + mail.show_edit_contact( + name, + contacts[mail.selected_idxs.contacts[name]].name, + contacts[mail.selected_idxs.contacts[name]].note + ) + end + + elseif fields.new then + mail.selected_idxs.contacts[name] = "#NEW#" + mail.show_edit_contact(name, "", "") + + elseif fields.edit and mail.selected_idxs.contacts[name] and contacts[mail.selected_idxs.contacts[name]] then + mail.show_edit_contact( + name, + contacts[mail.selected_idxs.contacts[name]].name, + contacts[mail.selected_idxs.contacts[name]].note + ) + + elseif fields.delete then + if contacts[mail.selected_idxs.contacts[name]] then + -- delete the contact and set the selected to the next in the list, + -- except if it was the last. Then determine the new last + local found = false + local last = nil + for k in mail.pairsByKeys(contacts) do + if found then + mail.selected_idxs.contacts[name] = k + break + elseif k == mail.selected_idxs.contacts[name] then + mail.delete_contact(name, contacts[mail.selected_idxs.contacts[name]].name) + mail.selected_idxs.contacts[name] = nil + found = true + else + last = k + end + end + if found and not mail.selected_idxs.contacts[name] then + -- was the last in the list, so take the previous (new last) + mail.selected_idxs.contacts[name] = last + end + end + + mail.show_contacts(name) + + elseif fields.back then + mail.show_mail_menu(name) + end + + return true +end) \ No newline at end of file diff --git a/ui/edit_contact.lua b/ui/edit_contact.lua new file mode 100644 index 0000000..15aa50a --- /dev/null +++ b/ui/edit_contact.lua @@ -0,0 +1,74 @@ +local FORMNAME = "mail:editcontact" + +function mail.show_edit_contact(name, contact_name, note, illegal_name_hint) + local formspec = [[ + size[6,7] + button[4,6.25;2,0.5;back;Back] + field[0.25,0.5;4,1;name;Player name:;%s] + textarea[0.25,1.6;4,6.25;note;Note:;%s] + button[4,0.10;2,1;save;Save] + ]] + if illegal_name_hint == "collision" then + formspec = formspec .. [[ + label[4,1;That name] + label[4,1.5;is already in] + label[4,2;your contacts.] + ]] + elseif illegal_name_hint == "empty" then + formspec = formspec .. [[ + label[4,1;The contact] + label[4,1.5;name cannot] + label[4,2;be empty.] + ]] + end + formspec = formspec .. mail.theme + formspec = string.format(formspec, + minetest.formspec_escape(contact_name or ""), + minetest.formspec_escape(note or "")) + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local contacts = mail.get_contacts(name) + + if fields.save then + if mail.selected_idxs.contacts[name] and mail.selected_idxs.contacts[name] ~= "#NEW#" then + local contact = contacts[mail.selected_idxs.contacts[name]] + if mail.selected_idxs.contacts[name] ~= string.lower(fields.name) then + -- name changed! + if #fields.name == 0 then + mail.show_edit_contact(name, contact.name, fields.note, "empty") + return true + + elseif contacts[string.lower(fields.name)] ~= nil then + mail.show_edit_contact(name, contact.name, fields.note, "collision") + return true + + else + mail.update_contact(name, contact) + contacts[mail.selected_idxs.contacts[name]] = nil + end + end + contact.name = fields.name + contact.note = fields.note + mail.update_contact(name, contact) + + else + mail.update_contact(name, { + name = fields.name, + note = fields.note, + }) + end + mail.show_contacts(name) + + elseif fields.back then + mail.show_contacts(name) + end + + return true +end) \ No newline at end of file diff --git a/ui/edit_maillists.lua b/ui/edit_maillists.lua new file mode 100644 index 0000000..64f7c9c --- /dev/null +++ b/ui/edit_maillists.lua @@ -0,0 +1,53 @@ +local FORMNAME = "mail:editmaillist" + +function mail.show_edit_maillist(playername, maillist_name, desc, players, illegal_name_hint) + local formspec = [[ + size[6,7] + button[4,6.25;2,0.5;back;Back] + field[0.25,0.5;4,1;name;Maillist name:;%s] + textarea[0.25,1.6;4,2;desc;Desc:;%s] + textarea[0.25,3.6;4,4.25;players;Players:;%s] + button[4,0.10;2,1;save;Save] + ]] + if illegal_name_hint == "collision" then + formspec = formspec .. [[ + label[4,1;That name] + label[4,1.5;is already in] + label[4,2;your maillists.] + ]] + elseif illegal_name_hint == "empty" then + formspec = formspec .. [[ + label[4,1;The maillist] + label[4,1.5;name cannot] + label[4,2;be empty.] + ]] + end + formspec = formspec .. mail.theme + formspec = string.format(formspec, + minetest.formspec_escape(maillist_name or ""), + minetest.formspec_escape(desc or ""), + minetest.formspec_escape(players or "")) + minetest.show_formspec(playername, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + if fields.save then + mail.update_maillist(name, { + owner = name, + name = fields.name, + desc = fields.desc, + players = mail.parse_player_list(fields.players) + }) + mail.show_maillists(name) + + elseif fields.back then + mail.show_maillists(name) + end + + return true +end) \ No newline at end of file diff --git a/ui/events.lua b/ui/events.lua new file mode 100644 index 0000000..04fc878 --- /dev/null +++ b/ui/events.lua @@ -0,0 +1,116 @@ +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= "mail:inbox" and formname ~= "mail:sent" then + return + end + + local name = player:get_player_name() + + -- split inbox and sent msgs for different tests + local entry = mail.get_storage_entry(name) + + local messagesInbox = entry.inbox + local messagesSent = entry.outbox + + if fields.inbox then -- inbox table + local evt = minetest.explode_table_event(fields.inbox) + mail.selected_idxs.inbox[name] = evt.row - 1 + if evt.type == "DCL" and messagesInbox[mail.selected_idxs.inbox[name]] then + mail.show_message(name, messagesInbox[mail.selected_idxs.inbox[name]].id) + end + return true + end + + if fields.sent then -- sent table + local evt = minetest.explode_table_event(fields.sent) + mail.selected_idxs.sent[name] = evt.row - 1 + if evt.type == "DCL" and messagesSent[mail.selected_idxs.sent[name]] then + mail.show_message(name, messagesSent[mail.selected_idxs.sent[name]].id) + end + return true + end + + if fields.boxtab == "1" then + mail.selected_idxs.boxtab[name] = 1 + mail.show_inbox(name) + + elseif fields.boxtab == "2" then + mail.selected_idxs.boxtab[name] = 2 + mail.show_sent(name) + + elseif fields.read then + if formname == "mail:inbox" and messagesInbox[mail.selected_idxs.inbox[name]] then -- inbox table + mail.show_message(name, messagesInbox[mail.selected_idxs.inbox[name]].id) + elseif formname == "mail:sent" and messagesSent[mail.selected_idxs.sent[name]] then -- sent table + mail.show_message(name, messagesSent[mail.selected_idxs.sent[name]].id) + end + + elseif fields.delete then + if formname == "mail:inbox" and messagesInbox[mail.selected_idxs.inbox[name]] then -- inbox table + mail.delete_mail(name, messagesInbox[mail.selected_idxs.inbox[name]].id) + elseif formname == "mail:sent" and messagesSent[mail.selected_idxs.sent[name]] then -- sent table + mail.delete_mail(name, messagesSent[mail.selected_idxs.sent[name]].id) + end + + mail.show_mail_menu(name) + + elseif fields.reply then + if formname == "mail:inbox" and messagesInbox[mail.selected_idxs.inbox[name]] then + local message = messagesInbox[mail.selected_idxs.inbox[name]] + mail.reply(name, message) + elseif formname == "mail:sent" and messagesSent[mail.selected_idxs.sent[name]] then + local message = messagesSent[mail.selected_idxs.sent[name]] + mail.reply(name, message) + end + + elseif fields.replyall then + if formname == "mail:inbox" and messagesInbox[mail.selected_idxs.inbox[name]] then + local message = messagesInbox[mail.selected_idxs.inbox[name]] + mail.replyall(name, message) + elseif formname == "mail:sent" and messagesSent[mail.selected_idxs.sent[name]] then + local message = messagesSent[mail.selected_idxs.sent[name]] + mail.replyall(name, message) + end + + elseif fields.forward then + if formname == "mail:inbox" and messagesInbox[mail.selected_idxs.inbox[name]] then + local message = messagesInbox[mail.selected_idxs.inbox[name]] + mail.forward(name, message) + elseif formname == "mail:sent" and messagesSent[mail.selected_idxs.sent[name]] then + local message = messagesSent[mail.selected_idxs.sent[name]] + mail.forward(name, message) + end + + elseif fields.markread then + if formname == "mail:inbox" and messagesInbox[mail.selected_idxs.inbox[name]] then + mail.mark_read(name, messagesInbox[mail.selected_idxs.inbox[name]].id) + elseif formname == "mail:sent" and messagesSent[mail.selected_idxs.sent[name]] then + mail.mark_read(name, messagesSent[mail.selected_idxs.sent[name]].id) + end + + mail.show_mail_menu(name) + + elseif fields.markunread then + if formname == "mail:inbox" and messagesInbox[mail.selected_idxs.inbox[name]] then + mail.mark_unread(name, messagesInbox[mail.selected_idxs.inbox[name]].id) + elseif formname == "mail:sent" and messagesSent[mail.selected_idxs.sent[name]] then + mail.mark_unread(name, messagesSent[mail.selected_idxs.sent[name]].id) + end + + mail.show_mail_menu(name) + + elseif fields.new then + mail.show_compose(name) + + elseif fields.contacts then + mail.show_contacts(name) + + elseif fields.maillists then + mail.show_maillists(name) + + elseif fields.about then + mail.show_about(name) + + end + + return true +end) diff --git a/ui/inbox.lua b/ui/inbox.lua new file mode 100644 index 0000000..5851003 --- /dev/null +++ b/ui/inbox.lua @@ -0,0 +1,66 @@ +local inbox_formspec = "size[8,10;]" .. mail.theme .. [[ + tabheader[0.3,1;boxtab;Inbox,Sent messages;1;false;false] + + button[6,0.10;2,0.5;new;New] + button[6,0.95;2,0.5;read;Read] + button[6,1.70;2,0.5;reply;Reply] + button[6,2.45;2,0.5;replyall;Reply All] + button[6,3.20;2,0.5;forward;Forward] + button[6,3.95;2,0.5;delete;Delete] + button[6,4.82;2,0.5;markread;Mark Read] + button[6,5.55;2,0.5;markunread;Mark Unread] + button[6,6.8;2,0.5;contacts;Contacts] + button[6,7.6;2,0.5;maillists;Mail lists] + button[6,8.7;2,0.5;about;About] + button_exit[6,9.5;2,0.5;quit;Close] + + tablecolumns[color;text;text] + table[0,0.7;5.75,9.35;inbox;#999,From,Subject]] + + +function mail.show_inbox(name) + local formspec = { inbox_formspec } + local entry = mail.get_storage_entry(name) + local messages = entry.inbox + + mail.message_drafts[name] = nil + + if messages[1] then + for _, message in ipairs(messages) do + if not message.read then + if not mail.player_in_list(name, message.to) then + formspec[#formspec + 1] = ",#FFD788" + else + formspec[#formspec + 1] = ",#FFD700" + end + else + if not mail.player_in_list(name, message.to) then + formspec[#formspec + 1] = ",#CCCCDD" + else + formspec[#formspec + 1] = "," + end + end + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = minetest.formspec_escape(message.from) + formspec[#formspec + 1] = "," + if message.subject ~= "" then + if string.len(message.subject) > 30 then + formspec[#formspec + 1] = minetest.formspec_escape(string.sub(message.subject, 1, 27)) + formspec[#formspec + 1] = "..." + else + formspec[#formspec + 1] = minetest.formspec_escape(message.subject) + end + else + formspec[#formspec + 1] = "(No subject)" + end + end + if mail.selected_idxs.inbox[name] then + formspec[#formspec + 1] = ";" + formspec[#formspec + 1] = tostring(mail.selected_idxs.inbox[name] + 1) + end + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "]label[2.25,4.5;No mail]" + end + minetest.show_formspec(name, "mail:inbox", table.concat(formspec, "")) +end diff --git a/ui/mail.lua b/ui/mail.lua new file mode 100644 index 0000000..47b92ad --- /dev/null +++ b/ui/mail.lua @@ -0,0 +1,10 @@ +-- helper function for tabbed overview + +function mail.show_mail_menu(playername) + local index = mail.selected_idxs.boxtab[playername] or 1 + if index == 1 then + mail.show_inbox(playername) + elseif index == 2 then + mail.show_sent(playername) + end +end \ No newline at end of file diff --git a/ui/maillists.lua b/ui/maillists.lua new file mode 100644 index 0000000..5a82b86 --- /dev/null +++ b/ui/maillists.lua @@ -0,0 +1,110 @@ +local FORMNAME = "mail:maillists" + +local maillists_formspec = "size[8,9;]" .. mail.theme .. [[ + button[6,0.10;2,0.5;new;New] + button[6,0.85;2,0.5;edit;Edit] + button[6,1.60;2,0.5;delete;Delete] + button[6,8.25;2,0.5;back;Back] + tablecolumns[color;text;text] + table[0,0;5.75,9;maillists;#999,Name,Description]] + +function mail.show_maillists(name) + local formspec = { maillists_formspec } + local maillists = mail.get_maillists(name) + + if maillists[1] then + for _, maillist in ipairs(maillists) do + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "@" .. minetest.formspec_escape(maillist.name) + formspec[#formspec + 1] = "," + if maillist.desc ~= "" then + if string.len(maillist.desc) > 30 then + formspec[#formspec + 1] = minetest.formspec_escape(string.sub(maillist.desc, 1, 27)) + formspec[#formspec + 1] = "..." + else + formspec[#formspec + 1] = minetest.formspec_escape(maillist.desc) + end + else + formspec[#formspec + 1] = "(No description)" + end + end + if mail.selected_idxs.maillists[name] then + formspec[#formspec + 1] = ";" + formspec[#formspec + 1] = mail.selected_idxs.maillists[name] + end + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "]label[2.25,4.5;No maillist]" + end + minetest.show_formspec(name, FORMNAME, table.concat(formspec, "")) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local maillists = mail.get_maillists(name) + + if fields.maillists then + local evt = minetest.explode_table_event(fields.maillists) + mail.selected_idxs.maillists[name] = evt.row - 1 + if evt.type == "DCL" and maillists[mail.selected_idxs.maillists[name]] then + local maillist = mail.get_maillist_by_name(name, maillists[mail.selected_idxs.maillists[name]].name) + local players_string = mail.concat_player_list(maillist.players) + mail.show_edit_maillist( + name, + maillists[mail.selected_idxs.maillists[name]].name, + maillists[mail.selected_idxs.maillists[name]].desc, + players_string + ) + end + + elseif fields.new then + mail.selected_idxs.maillists[name] = "#NEW#" + mail.show_edit_maillist(name, "", "", "Player1, Player2, Player3") + + elseif fields.edit and maillists[mail.selected_idxs.maillists[name]] then + local maillist = mail.get_maillist_by_name(name, maillists[mail.selected_idxs.maillists[name]].name) + local players_string = mail.concat_player_list(maillist.players) + mail.show_edit_maillist( + name, + maillists[mail.selected_idxs.maillists[name]].name, + maillists[mail.selected_idxs.maillists[name]].desc, + players_string + ) + + elseif fields.delete then + if maillists[mail.selected_idxs.maillists[name]] then + -- delete the maillist and set the selected to the next in the list, + -- except if it was the last. Then determine the new last + local found = false + local last = nil + for k in mail.pairsByKeys(maillists) do + if found then + mail.selected_idxs.maillists[name] = k + break + elseif k == mail.selected_idxs.maillists[name] then + mail.delete_maillist(maillists[mail.selected_idxs.maillists[name]].name) + mail.selected_idxs.maillists[name] = nil + found = true + else + last = k + end + end + if found and not mail.selected_idxs.maillists[name] then + -- was the last in the list, so take the previous (new last) + mail.selected_idxs.maillists[name] = last + end + end + + mail.show_maillists(name) + + elseif fields.back then + mail.show_mail_menu(name) + end + + return true +end) \ No newline at end of file diff --git a/ui/message.lua b/ui/message.lua new file mode 100644 index 0000000..3fb999f --- /dev/null +++ b/ui/message.lua @@ -0,0 +1,136 @@ +local FORMNAME = "mail:message" + +function mail.show_message(name, id) + local message = mail.get_message(name, id) + + local formspec = [[ + size[8,9] + + box[0,0;7,1.9;#466432] + + button[7.25,0.15;0.75,0.5;back;X] + + label[0.2,0.1;From: %s] + label[0.2,0.5;To: %s] + label[0.2,0.9;CC: %s] + label[0.2,1.3;Date: %s] + + label[0,2.1;Subject: %s] + textarea[0.25,2.6;8,7.0;;;%s] + + button[0,8.5;2,1;reply;Reply] + button[2,8.5;2,1;replyall;Reply All] + button[4,8.5;2,1;forward;Forward] + button[6,8.5;2,1;delete;Delete] + ]] .. mail.theme + + local from = minetest.formspec_escape(message.from) or "" + local to = minetest.formspec_escape(message.to) or "" + local cc = minetest.formspec_escape(message.cc) or "" + local date = type(message.time) == "number" + and minetest.formspec_escape(os.date("%Y-%m-%d %X", message.time)) or "" + local subject = minetest.formspec_escape(message.subject) or "" + local body = minetest.formspec_escape(message.body) or "" + formspec = string.format(formspec, from, to, cc, date, subject, body) + + if not message.read then + -- mark as read + mail.mark_read(name, id) + end + + minetest.show_formspec(name, FORMNAME, formspec) +end + +function mail.reply(name, message) + local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body + mail.show_compose(name, message.from, "Re: "..message.subject, replyfooter) +end + +function mail.replyall(name, message) + local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body + + -- new recipients are the sender plus the original recipients, minus ourselves + local recipients = message.to or "" + if message.from ~= nil then + recipients = message.from .. ", " .. recipients + end + recipients = mail.parse_player_list(recipients) + for k,v in pairs(recipients) do + if v == name then + table.remove(recipients, k) + break + end + end + recipients = mail.concat_player_list(recipients) + + -- new CC is old CC minus ourselves + local cc = mail.parse_player_list(message.cc) + for k,v in pairs(cc) do + if v == name then + table.remove(cc, k) + break + end + end + cc = mail.concat_player_list(cc) + + mail.show_compose(name, recipients, "Re: "..message.subject, replyfooter, cc) +end + +function mail.forward(name, message) + local fwfooter = "Type your message here.\n\n--Original message follows--\n" .. (message.body or "") + mail.show_compose(name, "", "Fw: " .. (message.subject or ""), fwfooter) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local entry = mail.get_storage_entry(name) + + local messagesInbox = entry.inbox + local messagesSent = entry.outbox + + if fields.back then + mail.show_mail_menu(name) + return true -- don't uselessly set messages + + elseif fields.reply then + local message = "" + if messagesInbox[mail.selected_idxs.inbox[name]] then + message = messagesInbox[mail.selected_idxs.inbox[name]] + elseif messagesSent[mail.selected_idxs.sent[name]] then + message = messagesSent[mail.selected_idxs.sent[name]] + end + mail.reply(name, message) + + elseif fields.replyall then + local message = "" + if messagesInbox[mail.selected_idxs.inbox[name]] then + message = messagesInbox[mail.selected_idxs.inbox[name]] + elseif messagesSent[mail.selected_idxs.sent[name]] then + message = messagesSent[mail.selected_idxs.sent[name]] + end + mail.replyall(name, message) + + elseif fields.forward then + local message = "" + if messagesInbox[mail.selected_idxs.inbox[name]] then + message = messagesInbox[mail.selected_idxs.inbox[name]] + elseif messagesSent[mail.selected_idxs.sent[name]] then + message = messagesSent[mail.selected_idxs.sent[name]] + end + mail.forward(name, message) + + elseif fields.delete then + if messagesInbox[mail.selected_idxs.inbox[name]] then + mail.delete_mail(name, messagesInbox[mail.selected_idxs.inbox[name]].id) + elseif messagesSent[mail.selected_idxs.sent[name]] then + mail.delete_mail(name, messagesSent[mail.selected_idxs.sent[name]].id) + end + mail.show_mail_menu(name) + end + + return true +end) \ No newline at end of file diff --git a/ui/outbox.lua b/ui/outbox.lua new file mode 100644 index 0000000..ae980a7 --- /dev/null +++ b/ui/outbox.lua @@ -0,0 +1,53 @@ + +local sent_formspec = "size[8,10;]" .. mail.theme .. [[ + tabheader[0.3,1;boxtab;Inbox,Sent messages;2;false;false] + + button[6,0.10;2,0.5;new;New] + button[6,0.95;2,0.5;read;Read] + button[6,1.70;2,0.5;reply;Reply] + button[6,2.45;2,0.5;replyall;Reply All] + button[6,3.20;2,0.5;forward;Forward] + button[6,3.95;2,0.5;delete;Delete] + button[6,6.8;2,0.5;contacts;Contacts] + button[6,7.6;2,0.5;maillists;Mail lists] + button[6,8.7;2,0.5;about;About] + button_exit[6,9.5;2,0.5;quit;Close] + + tablecolumns[color;text;text] + table[0,0.7;5.75,9.35;sent;#999,To,Subject]] + + +function mail.show_sent(name) + local formspec = { sent_formspec } + local entry = mail.get_storage_entry(name) + local messages = entry.outbox + + mail.message_drafts[name] = nil + + if messages[1] then + for _, message in ipairs(messages) do + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = minetest.formspec_escape(message.to) + formspec[#formspec + 1] = "," + if message.subject ~= "" then + if string.len(message.subject) > 30 then + formspec[#formspec + 1] = minetest.formspec_escape(string.sub(message.subject, 1, 27)) + formspec[#formspec + 1] = "..." + else + formspec[#formspec + 1] = minetest.formspec_escape(message.subject) + end + else + formspec[#formspec + 1] = "(No subject)" + end + end + if mail.selected_idxs.sent[name] then + formspec[#formspec + 1] = ";" + formspec[#formspec + 1] = tostring(mail.selected_idxs.sent[name] + 1) + end + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "]label[2.25,4.5;No mail]" + end + minetest.show_formspec(name, "mail:sent", table.concat(formspec, "")) +end diff --git a/ui/select_contact.lua b/ui/select_contact.lua new file mode 100644 index 0000000..74494a5 --- /dev/null +++ b/ui/select_contact.lua @@ -0,0 +1,115 @@ +local FORMNAME = "mail:selectcontact" + +local select_contact_formspec = "size[8,9;]" .. mail.theme .. [[ + tablecolumns[color;text;text] + table[0,0;3.5,9;contacts;#999,Name,Note%s] + button[3.55,2.00;1.75,0.5;toadd;→ Add] + button[3.55,2.75;1.75,0.5;toremove;← Remove] + button[3.55,6.00;1.75,0.5;ccadd;→ Add] + button[3.55,6.75;1.75,0.5;ccremove;← Remove] + tablecolumns[color;text;text] + table[5.15,0.0;2.75,4.5;to;#999,TO:,Note%s] + tablecolumns[color;text;text] + table[5.15,4.6;2.75,4.5;cc;#999,CC:,Note%s] + button[3.55,8.25;1.75,0.5;back;Back] +]] + + +function mail.show_select_contact(name, to, cc) + local formspec = select_contact_formspec + local contacts = mail.compile_contact_list(name, mail.selected_idxs.contacts[name]) + + -- compile lists + if to then + to = mail.compile_contact_list(name, mail.selected_idxs.to[name], to) + else + to = "" + end + if cc then + cc = mail.compile_contact_list(name, mail.selected_idxs.cc[name], cc) + else + cc = "" + end + --[[if bcc then + bcc = table.concat(mail.compile_contact_list(name, mail.selected_idxs.bcc[name], bcc) + else + bcc = "" + end]]-- + formspec = string.format(formspec, contacts, to, cc)--, bcc() + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local contacts = mail.get_contacts(name) + local draft = mail.message_drafts[name] + + -- get indexes for fields with selected rows + -- execute their default button's actions if double clicked + for k,action in pairs({ + contacts = "toadd", + to = "toremove", + cc = "ccremove", + bcc = "bccremove" + }) do + if fields[k] then + local evt = minetest.explode_table_event(fields[k]) + mail.selected_idxs[k][name] = evt.row - 1 + if evt.type == "DCL" and mail.selected_idxs[k][name] then + fields[action] = true + end + return true + end + end + + local update = false + -- add + for _,v in pairs({"to","cc","bcc"}) do + if fields[v.."add"] then + update = true + if mail.selected_idxs.contacts[name] then + for k, contact, i in mail.pairsByKeys(contacts) do + if k == mail.selected_idxs.contacts[name] or i == mail.selected_idxs.contacts[name] then + local list = mail.parse_player_list(draft[v]) + list[#list+1] = contact.name + mail.selected_idxs[v][name] = #list + draft[v] = mail.concat_player_list(list) + break + end + end + end + end + end + -- remove + for _,v in pairs({"to","cc","bcc"}) do + if fields[v.."remove"] then + update = true + if mail.selected_idxs[v][name] then + local list = mail.parse_player_list(draft[v]) + table.remove(list, mail.selected_idxs[v][name]) + if #list < mail.selected_idxs[v][name] then + mail.selected_idxs[v][name] = #list + end + draft[v] = mail.concat_player_list(list) + end + end + end + + if update then + mail.show_select_contact(name, draft.to, draft.cc, draft.bcc) + return true + end + + -- delete old idxs + for _,v in ipairs({"contacts","to","cc","bcc"}) do + mail.selected_idxs[v][name] = nil + end + + mail.show_compose(name, draft.to, draft.subject, draft.body, draft.cc, draft.bcc) + + return true +end) \ No newline at end of file diff --git a/util/channel.lua b/util/channel.lua deleted file mode 100644 index e879adf..0000000 --- a/util/channel.lua +++ /dev/null @@ -1,91 +0,0 @@ --- bi-directional http-channel --- with long-poll GET and POST on the same URL - -local function Channel(http, url, cfg) - cfg = cfg or {} - local extra_headers = cfg.extra_headers or {} - local timeout = cfg.timeout or 1 - local long_poll_timeout = cfg.long_poll_timeout or 30 - local error_retry = cfg.error_retry or 10 - - -- assemble post-header with json content - local post_headers = { "Content-Type: application/json" } - for _,header in pairs(cfg.extra_headers) do - table.insert(post_headers, header) - end - - local recv_listeners = {} - local run = true - - local recv_loop - - recv_loop = function() - assert(run) - - -- long-poll GET - http.fetch({ - url = url, - extra_headers = extra_headers, - timeout = long_poll_timeout - }, function(res) - if res.succeeded and res.code == 200 then - local data = minetest.parse_json(res.data) - - if data then - for _,listener in pairs(recv_listeners) do - if #data > 0 then - -- array received - for _, entry in ipairs(data) do - listener(entry) - end - else - -- single item received - listener(data) - end - end - end - -- reschedule immediately - minetest.after(0, recv_loop) - else - -- error, retry after some time - minetest.after(error_retry, recv_loop) - end - end) - end - - - local send = function(data) - assert(run) - -- POST - - http.fetch({ - url = url, - extra_headers = post_headers, - timeout = timeout, - post_data = minetest.write_json(data) - }, function() - -- TODO: error-handling - end) - end - - local receive = function(listener) - table.insert(recv_listeners, listener) - end - - local close = function() - run = false - end - - recv_loop(); - - return { - send = send, - receive = receive, - close = close - } - -end - - - -return Channel diff --git a/util/normalize.lua b/util/normalize.lua index 3064bf3..b817068 100644 --- a/util/normalize.lua +++ b/util/normalize.lua @@ -6,11 +6,11 @@ and add individual player names to recipient list --]] function mail.normalize_players_and_add_recipients(field, recipients, undeliverable) local order = mail.parse_player_list(field) - for _, player_name in ipairs(order) do - if not minetest.player_exists(player_name) then - undeliverable[player_name] = true + for _, recipient_name in ipairs(order) do + if not minetest.player_exists(recipient_name) then + undeliverable[recipient_name] = true else - recipients[player_name] = true + recipients[recipient_name] = true end end return mail.concat_player_list(order) @@ -59,9 +59,3 @@ function mail.player_in_list(name, list) end return false end - -function mail.ensure_new_format(message, name) - if message.to == nil then - message.to = name - end -end diff --git a/util/normalize.spec.lua b/util/normalize.spec.lua new file mode 100644 index 0000000..88628ad --- /dev/null +++ b/util/normalize.spec.lua @@ -0,0 +1,12 @@ + +mtt.register("util/normalize_players_and_add_recipients", function(callback) + local recipients = {} + local undeliverable = {} + local to = mail.normalize_players_and_add_recipients("player1,player2", recipients, undeliverable) + + assert(to == "player1, player2") + assert(not next(undeliverable)) + assert(recipients["player1"]) + assert(recipients["player2"]) + callback() +end) \ No newline at end of file diff --git a/util/uuid.lua b/util/uuid.lua new file mode 100644 index 0000000..571aa4a --- /dev/null +++ b/util/uuid.lua @@ -0,0 +1,9 @@ +-- source: https://gist.github.com/jrus/3197011 +local random = math.random +function mail.new_uuid() + local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + return string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and random(0, 0xf) or random(8, 0xb) + return string.format('%x', v) + end) +end \ No newline at end of file diff --git a/util/uuid.spec.lua b/util/uuid.spec.lua new file mode 100644 index 0000000..549399e --- /dev/null +++ b/util/uuid.spec.lua @@ -0,0 +1,7 @@ + + +mtt.register("uuid", function(callback) + assert(mail.new_uuid()) + assert(mail.new_uuid() ~= mail.new_uuid()) + callback() +end) \ No newline at end of file