View sent messages (new database, add maillists) (#26)

* Add tabheader & sent formspec

* Add show_sent function and show sent messages

* Remove comment on selected_idxs test (show_sent)

* Add variable to keep the previous tab instead of going back to the first one

* Remove index variable verification on mark read/unread buttons since they are necessarily clicked on inbox view

* Resize messages table to be aligned with close button at the bottom

* Show date time (#27)

* Show date in message reading

* Fix wrong registered dates

Co-authored-by: SX <50966843+S-S-X@users.noreply.github.com>

* Rework header layout to add better space for date

---------

Co-authored-by: SX <50966843+S-S-X@users.noreply.github.com>

* Add insertion of messages into global storage mail.messages.json

* Receive player messages from global storage

* Add automatic generation of status for a new message (unread)

* Mark read/unread/delete a message

* Fix messages loading

* Show every message received/sent via specific functions

* Use global contacts functions and reconfigure add/remove functions

* Create mail lists formspec based on contacts

* Add deleting contact

* Add ability to create mail lists

* Fix inability to edit contact

* Rework on editing/deletion of contacts/maillists

* Add at symbol as prefix on maillists view

* Add ability to choose default status (to/cc/bcc)

Signed-off-by: Athozus <athozus@gmail.com>

* Add ability to add multiples players and choose their default status (to/cc/bcc)

* Add ability to use maillist in messages and receive messages from them

* Fix repetition of code causing a crash

* Avoid multiples occurences of the same messages due to player both in maillist and receivers

* Fix selected indexes for inbox/sent

Now separated, fixed show_message() func selection of id from table dcl/read btn

* Fix many issues related to maillists

Notably : edit, delete, selection, creation, registration of players

* Set up database version v3 and its migration from v2

+ Check versions to choose v1->v2 or v2->v3

* Fix mtt.lua

Due to old function getMessages(), replaced by getPlayerInboxMessages()

* Add 10 seconds security to mtt.lua

* Fix migrate.lua non-declared variable

* Send msg table with string keys in mtt

* Better log messages

* Add message check

* Fix mtt crash

* Better syntax in storage.lua

* Fix bcc forgotten in mail.send()

* Fix mtt issue

* Better compatibility for messages storage

Co-authored-by: SX <50966843+S-S-X@users.noreply.github.com>

* Replace mail.split by builtin func

Co-authored-by: SX <50966843+S-S-X@users.noreply.github.com>

* Use builtin split func

Co-authored-by: SX <50966843+S-S-X@users.noreply.github.com>

* Use builtin split func in storage.lua

* re-add mtt if

* luacheck on PR

* add check for an ancient issue with missing `to` field

* Fix luacheck on storage.lua

* Fix luacheck warnings in migrate.lua

* Fix luacheck warnings in gui.lua

* Fix luacheck (too long lines) in storage.lua

* Unused loop values in migrate.lua

* Whitespace line in gui.lua

* Whitespace line (init.lua)

* Whitespace line (api.lua)

* Significantly improve maillist behaviour

Replace maillist by its players when sending a message
List of players separated by ,
Avoid doublons when editing more than 2 times a maillist

* Fix luacheck

* Fix table insertions at first index when no needed

* Use funcs

* Do not add maillist as a new contact when sending a mail

* Fix removing elements from tables

* Check maillists not added in contacts

* storage rewrite wip

* storage format docs

* refactor ui components

* show_compose cleanup

* remove unused channel.lua

* error -> err

* status refactoring

* contacts refactoring

* maillist refactoring

* docs

* tests

* fix some issues

* re-enable migrations

* contributors

* prefix mail entries in the mod storage

* internalize old mail-paths to migration module

* add v1 and v2 player db examples and migration test

* Ui improvements & fixes

Move events code (if fields.x then) to events.lua (instead of inbox.lua), fix tab selection when going backward

* Show most recent messages at first (outbox)

* unified-inv fix

---------

Signed-off-by: Athozus <athozus@gmail.com>
Co-authored-by: SX <50966843+S-S-X@users.noreply.github.com>
Co-authored-by: BuckarooBanzay <BuckarooBanzay@users.noreply.github.com>
This commit is contained in:
Athozus 2023-03-29 17:25:01 +02:00 committed by GitHub
parent b0a5bc7e47
commit b3e0c158f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1500 additions and 960 deletions

View File

@ -1,6 +1,6 @@
name: luacheck name: luacheck
on: [push] on: [push, pull_request]
jobs: jobs:
build: build:

View File

@ -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 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 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 `<worldfolder>/mails.db` file
* `v2` every player has its own (in-) mailbox in the `<worldfolder>/mails/<playername>.json` file
* `v3` every player has an entry in the `<playername>` modstorage (inbox, outbox, contacts)
# Dependencies # Dependencies
* None * None
@ -43,6 +50,12 @@ See the "LICENSE" file
* Cheapie (initial idea/project) * Cheapie (initial idea/project)
* Rubenwardy (lua/ui improvements) * 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/Historic stuff
* Old forum topic: https://forum.minetest.net/viewtopic.php?t=14464 * Old forum topic: https://forum.minetest.net/viewtopic.php?t=14464

70
api.lua
View File

@ -10,36 +10,15 @@ end
mail.receive_mail_message = "You have a new message from %s! Subject: %s\nTo view it, type /mail" 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.read_later_message = "You can read your messages later by using the /mail command"
--[[ function mail.send(m)
mail sending function, can be invoked with one object argument (new api) or if type(m.from) ~= "string" then return false, "'from' is not a string" end
all 4 parameters (old compat version) if type(m.to) ~= "string" then return false, "'to' is not a string" end
see: "Mail format" api.md if type(m.body) ~= "string" then return false, "'body' is not a string" end
TODO: refactor this garbage code! -- defaults
--]] m.subject = m.subject or "(No subject)"
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
-- sane default values -- limit subject line
m.subject = m.subject or ""
m.body = m.body or ""
if m.subject == "" then
m.subject = "(No subject)"
end
if string.len(m.subject) > 30 then if string.len(m.subject) > 30 then
m.subject = string.sub(m.subject,1,27) .. "..." m.subject = string.sub(m.subject,1,27) .. "..."
end end
@ -47,11 +26,14 @@ function mail.send(...)
-- normalize to, cc and bcc while compiling a list of all recipients -- normalize to, cc and bcc while compiling a list of all recipients
local recipients = {} local recipients = {}
local undeliverable = {} 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) m.to = mail.normalize_players_and_add_recipients(m.to, recipients, undeliverable)
if m.cc then 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) m.cc = mail.normalize_players_and_add_recipients(m.cc, recipients, undeliverable)
end end
if m.bcc then 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) m.bcc = mail.normalize_players_and_add_recipients(m.bcc, recipients, undeliverable)
end end
@ -60,9 +42,7 @@ function mail.send(...)
for name in pairs(undeliverable) do for name in pairs(undeliverable) do
undeliverable_names[#undeliverable_names + 1] = '"' .. name .. '"' undeliverable_names[#undeliverable_names + 1] = '"' .. name .. '"'
end end
return f("recipients %s don't exist; cannot send mail.", return false, f("recipients %s don't exist; cannot send mail.", table.concat(undeliverable_names, ", "))
table.concat(undeliverable_names, ", ")
)
end end
local extra = {} local extra = {}
@ -85,20 +65,26 @@ function mail.send(...)
-- form the actual mail -- form the actual mail
local msg = { local msg = {
unread = true, id = mail.new_uuid(),
sender = m.from, from = m.from,
to = m.to, to = m.to,
cc = m.cc, cc = m.cc,
bcc = m.bcc,
subject = m.subject, subject = m.subject,
body = m.body, body = m.body,
time = os.time(), 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 for recipient in pairs(recipients) do
local messages = mail.getMessages(recipient) entry = mail.get_storage_entry(recipient)
table.insert(messages, 1, msg) table.insert(entry.inbox, msg)
mail.setMessages(recipient, messages) mail.set_storage_entry(recipient, entry)
end end
-- notify recipients that happen to be online -- notify recipients that happen to be online
@ -115,4 +101,6 @@ function mail.send(...)
break break
end end
end end
return true
end end

113
api.md
View File

@ -9,47 +9,28 @@ mail = {
cc = "carbon copy", cc = "carbon copy",
bcc = "players, which, get, a, copy, but, are, not, visible, to, others", bcc = "players, which, get, a, copy, but, are, not, visible, to, others",
subject = "subject line", subject = "subject line",
body = "mail body", 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`. 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.
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. 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`.
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"}
}
```
## Sending mail ## 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 ```lua
local error = mail.send({ local success, error = mail.send({
from = "sender name", from = "singleplayer",
to = "destination name", to = "playername",
cc = "carbon copy", cc = "carbon, copy",
bcc = "blind carbon copy", bcc = "blind, carbon, copy",
subject = "subject line", subject = "subject line",
body = "mail body" 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 # Hooks
@ -61,22 +42,56 @@ mail.register_on_receive(function(m)
end) end)
``` ```
# internal mail format (on-disk) # Internals
The mail format on-disk
> (worldfolder)/mails/(playername).json mod-storage entry for a player (indexed by playername and serialized with json):
```lua
```json {
[{ contacts = {
"unread": true, {
"sender": "sender name", -- name of the player (unique key in the list)
"subject": "subject name", name = "",
"body": "main\nmultiline\nbody", -- note
"time": 1551258349, note = ""
"attachments": [ },{
"default:stone 99", ...
"default:gold_ingot 99" }
] },
}] 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"}
}
}
}

12
api.spec.lua Normal file
View File

@ -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)

646
gui.lua
View File

@ -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) 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) -- TODO: refactor this - not just compiles *a* list, but *the* list for the contacts screen (too inflexible)
local formspec = {} local formspec = {}
local contacts = mail.getContacts(name) local contacts = mail.get_contacts(name)
if playernames == nil then if playernames == nil then
local length = 0 local length = 0
@ -250,465 +69,9 @@ function mail.compile_contact_list(name, selected, playernames)
formspec[#formspec + 1] = "]" formspec[#formspec + 1] = "]"
end end
return table.concat(formspec, "") return table.concat(formspec, "")
end 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 if minetest.get_modpath("unified_inventory") then
mail.receive_mail_message = mail.receive_mail_message .. mail.receive_mail_message = mail.receive_mail_message ..
" or use the mail button in the inventory" " or use the mail button in the inventory"
@ -718,6 +81,9 @@ if minetest.get_modpath("unified_inventory") then
unified_inventory.register_button("mail", { unified_inventory.register_button("mail", {
type = "image", type = "image",
image = "mail_button.png", image = "mail_button.png",
tooltip = "Mail" tooltip = "Mail",
action = function(player)
mail.show_mail_menu(player:get_player_name())
end
}) })
end end

View File

@ -1,15 +1,38 @@
mail = { mail = {
-- api version -- version
apiversion = 1.1, version = 3,
-- mail directory -- mod storage
maildir = minetest.get_worldpath().."/mails", storage = minetest.get_mod_storage(),
contactsdir = minetest.get_worldpath().."/mails/contacts"
-- 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()) local MP = minetest.get_modpath(minetest.get_current_modname())
dofile(MP .. "/util/normalize.lua") dofile(MP .. "/util/normalize.lua")
dofile(MP .. "/util/uuid.lua")
dofile(MP .. "/chatcommands.lua") dofile(MP .. "/chatcommands.lua")
dofile(MP .. "/migrate.lua") dofile(MP .. "/migrate.lua")
dofile(MP .. "/hud.lua") dofile(MP .. "/hud.lua")
@ -17,10 +40,26 @@ dofile(MP .. "/storage.lua")
dofile(MP .. "/api.lua") dofile(MP .. "/api.lua")
dofile(MP .. "/gui.lua") dofile(MP .. "/gui.lua")
dofile(MP .. "/onjoin.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 -- migrate storage
mail.migrate() mail.migrate()
if minetest.get_modpath("mtt") then if minetest.get_modpath("mtt") then
dofile(MP .. "/mtt.lua") dofile(MP .. "/mtt.lua")
end dofile(MP .. "/api.spec.lua")
dofile(MP .. "/migrate.spec.lua")
dofile(MP .. "/util/uuid.spec.lua")
dofile(MP .. "/util/normalize.spec.lua")
end

View File

@ -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() function mail.migrate()
-- create directory, just in case -- check for v2 storage first, v1-migration might have set the v3-flag already
minetest.mkdir(mail.maildir) local version = mail.storage:get_int(STORAGE_VERSION_KEY)
minetest.mkdir(mail.contactsdir) if version < 3 then
-- v2 to v3
local file = io.open(minetest.get_worldpath().."/mail.db", "r") migrate_v2_to_v3()
if file then mail.storage:set_int(STORAGE_VERSION_KEY, 3)
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")
end end
end -- check for v1 storage
local v1_file = io.open(minetest.get_worldpath().."/mail.db", "r")
if v1_file then
function mail.migrate_contacts(playername) -- v1 to v3
local file = io.open(mail.getContactsFile(playername), 'r') migrate_v1_to_v3()
if not file then mail.storage:set_int(STORAGE_VERSION_KEY, 3)
-- 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?
end end
end end

28
migrate.spec.lua Normal file
View File

@ -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)

12
mtt.lua
View File

@ -1,14 +1,10 @@
mtt.register("send mail", function(callback) mtt.register("setup", function(callback)
-- create "player2" -- create test players
local auth_handler = minetest.get_auth_handler() local auth_handler = minetest.get_auth_handler()
auth_handler.set_password("player1", "")
auth_handler.set_password("player2", "") 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() callback()
end) end)

View File

@ -1,6 +1,7 @@
minetest.register_on_joinplayer(function(player) minetest.register_on_joinplayer(function(player)
minetest.after(2, function(name) minetest.after(2, function(name)
local messages = mail.getMessages(name) local entry = mail.get_storage_entry(name)
local messages = entry.inbox
local unreadcount = 0 local unreadcount = 0
@ -16,6 +17,4 @@ minetest.register_on_joinplayer(function(player)
end end
end, player:get_player_name()) end, player:get_player_name())
mail.migrate_contacts(player:get_player_name())
end) end)

View File

@ -1,47 +1,195 @@
-- storage getter/setter
local STORAGE_PREFIX = "mail/"
function mail.getMailFile(playername) -- create or populate empty fields on an entry
local saneplayername = string.gsub(playername, "[.|/]", "") local function populate_entry(e)
return mail.maildir .. "/" .. saneplayername .. ".json" 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 end
function mail.getContactsFile(playername) function mail.get_storage_entry(playername)
local saneplayername = string.gsub(playername, "[.|/]", "") local str = mail.storage:get_string(STORAGE_PREFIX .. playername)
return mail.maildir .. "/contacts/" .. saneplayername .. ".json" if str == "" then
end -- new entry
return populate_entry()
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
else else
minetest.log("error","[mail] Save failed - messages may be lost! ("..playername..")") -- deserialize existing entry
return false local e = minetest.parse_json(str)
return populate_entry(e)
end end
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) -- get a mail by id from the players in- or outbox
return mail.read_json_file(mail.getContactsFile(playername)) 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 end
function mail.pairsByKeys(t, f) function mail.pairsByKeys(t, f)
@ -63,33 +211,3 @@ function mail.pairsByKeys(t, f)
return iter return iter
end 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

45
storage.spec.lua Normal file
View File

@ -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)

View File

@ -1,6 +1,13 @@
ARG ENGINE_VERSION=5.5.0 ARG ENGINE_VERSION=5.5.0
FROM registry.gitlab.com/minetest/minetest/server:${ENGINE_VERSION} 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 USER root
RUN apk add git &&\ RUN apk add git &&\
mkdir -p /root/.minetest/worlds/world/worldmods/ &&\ mkdir -p /root/.minetest/worlds/world/worldmods/ &&\

BIN
test/auth.sqlite Normal file

Binary file not shown.

1
test/mail.db Normal file
View File

@ -0,0 +1 @@
local _={};_[1]="singleplayer";return {old_v1_player={{unread=true,subject="test1",sender=_[1],body="test2"}},[_[1]]={}}

View File

@ -1,3 +1,4 @@
default_game = minetest_game default_game = minetest_game
mg_name = v7 mg_name = v7
mtt_enable = true mtt_enable = true
mtt_filter = mail

1
test/old_v2_player.json Normal file
View File

@ -0,0 +1 @@
[{"body":"test2","sender":"someone-else","subject":"test1","time":1678467148,"unread":false}]

27
ui/about.lua Normal file
View File

@ -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)

89
ui/compose.lua Normal file
View File

@ -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)

83
ui/contacts.lua Normal file
View File

@ -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)

74
ui/edit_contact.lua Normal file
View File

@ -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)

53
ui/edit_maillists.lua Normal file
View File

@ -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)

116
ui/events.lua Normal file
View File

@ -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)

66
ui/inbox.lua Normal file
View File

@ -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

10
ui/mail.lua Normal file
View File

@ -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

110
ui/maillists.lua Normal file
View File

@ -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)

136
ui/message.lua Normal file
View File

@ -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)

53
ui/outbox.lua Normal file
View File

@ -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

115
ui/select_contact.lua Normal file
View File

@ -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)

View File

@ -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

View File

@ -6,11 +6,11 @@ and add individual player names to recipient list
--]] --]]
function mail.normalize_players_and_add_recipients(field, recipients, undeliverable) function mail.normalize_players_and_add_recipients(field, recipients, undeliverable)
local order = mail.parse_player_list(field) local order = mail.parse_player_list(field)
for _, player_name in ipairs(order) do for _, recipient_name in ipairs(order) do
if not minetest.player_exists(player_name) then if not minetest.player_exists(recipient_name) then
undeliverable[player_name] = true undeliverable[recipient_name] = true
else else
recipients[player_name] = true recipients[recipient_name] = true
end end
end end
return mail.concat_player_list(order) return mail.concat_player_list(order)
@ -59,9 +59,3 @@ function mail.player_in_list(name, list)
end end
return false return false
end end
function mail.ensure_new_format(message, name)
if message.to == nil then
message.to = name
end
end

12
util/normalize.spec.lua Normal file
View File

@ -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)

9
util/uuid.lua Normal file
View File

@ -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

7
util/uuid.spec.lua Normal file
View File

@ -0,0 +1,7 @@
mtt.register("uuid", function(callback)
assert(mail.new_uuid())
assert(mail.new_uuid() ~= mail.new_uuid())
callback()
end)