Added a mail system.

master
Nathan Salapat 2021-04-15 10:43:07 -05:00
parent 130c824035
commit d14a8ce1cc
39 changed files with 1839 additions and 0 deletions

13
mods/mail_mod/LICENSE Normal file
View File

@ -0,0 +1,13 @@
The file textures/mail_button.png was created by bas080 and is licensed under the WTFPL.
Webmail component:
WTFPL
All other files:
Copyright (c) 2016 Carter Kolwey ("Cheapie Systems")
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and/or any associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

49
mods/mail_mod/README.md Normal file
View File

@ -0,0 +1,49 @@
Mail mod for Minetest (ingame mod)
======
![](https://github.com/minetest-mail/mail_mod/workflows/integration-test/badge.svg)
![](https://github.com/minetest-mail/mail_mod/workflows/luacheck/badge.svg)
This is a fork of cheapies mail mod
It adds a mail-system that allows players to send each other messages in-game and via webmail (optional)
# Screenshots
Ingame mail
![](pics/ingame.png?raw=true)
# Installation
## In-game mail mod
Install it like any other mod: copy the directory `mail_mod` to your "worldmods" folder
## Webmail
See: https://github.com/minetest-mail/mail
# Commands/Howto
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
# Dependencies
* None
# License
See the "LICENSE" file
# Textures
* textures/email_mail.png (https://github.com/rubenwardy/email.git WTFPL)
# Contributors
* Cheapie (initial idea/project)
* Rubenwardy (lua/ui improvements)
# Old/Historic stuff
* Old forum topic: https://forum.minetest.net/viewtopic.php?t=14464
* Old mod: https://cheapiesystems.com/git/mail/

121
mods/mail_mod/api.lua Normal file
View File

@ -0,0 +1,121 @@
-- see: mail.md
mail.registered_on_receives = {}
function mail.register_on_receive(func)
mail.registered_on_receives[#mail.registered_on_receives + 1] = func
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
TODO: refactor this garbage code!
--]]
function mail.send(src, dst, subject, body)
-- figure out format
local m
if dst == nil and subject == nil and body == nil then
-- new format (one object param)
m = src
else
-- old format
m = {}
m.from = src
m.to = dst
m.subject = subject
m.body = body
end
if m.dst and not m.to then
-- populate "to" field
m.to = m.dst
end
if m.src and not m.from then
-- populate "from" field
m.from = m.src
end
-- sane default values
m.subject = m.subject or ""
m.body = m.body or ""
local cc
local bcc
local extra
-- log mail send action
if m.cc or m.bcc then
if m.cc then
cc = "CC: " .. m.cc
if m.bcc then
cc = cc .. " - "
end
else
cc = ""
end
if m.bcc then
bcc = "BCC: " .. m.bcc
else
bcc = ""
end
extra = " (" .. cc .. bcc .. ")"
else
extra = ""
end
minetest.log("action", "[mail] '" .. m.from .. "' sends mail to '" .. m.to .. "'" ..
extra .. "' with subject '" .. m.subject .. "' and body: '" .. m.body .. "'")
-- normalize to, cc and bcc while compiling a list of all recipients
local recipients = {}
m.to = mail.normalize_players_and_add_recipients(m.to, recipients)
if m.cc then
m.cc = mail.normalize_players_and_add_recipients(m.cc, recipients)
end
if m.bcc then
m.bcc = mail.normalize_players_and_add_recipients(m.bcc, recipients)
end
-- form the actual mail
local msg = {
unread = true,
sender = m.from,
to = m.to,
subject = m.subject,
body = m.body,
time = os.time(),
}
if m.cc then
msg.cc = m.cc
end
-- send the mail to all recipients
for _, recipient in pairs(recipients) do
local messages = mail.getMessages(recipient)
table.insert(messages, 1, msg)
mail.setMessages(recipient, messages)
end
-- notify recipients that happen to be online
for _, player in ipairs(minetest.get_connected_players()) do
local name = player:get_player_name()
if recipients[string.lower(name)] ~= nil then
if m.subject == "" then m.subject = "(No subject)" end
if string.len(m.subject) > 30 then
m.subject = string.sub(m.subject,1,27) .. "..."
end
minetest.chat_send_player(name,
string.format(mail.receive_mail_message, m.from, m.subject))
end
end
for i=1, #mail.registered_on_receives do
if mail.registered_on_receives[i](m) then
break
end
end
end

80
mods/mail_mod/api.md Normal file
View File

@ -0,0 +1,80 @@
# Mail format
The mail format in the api hooks
```lua
mail = {
from = "sender name",
to = "players, which, are, addressed",
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"}
}
```
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"}
}
```
## Sending mail
Old variant (pre-1.1)
```lua
mail.send("source name", "destination name", "subject line", "mail body")
```
New variant (1.1+)
```lua
mail.send({
from = "sender name",
to = "destination name",
cc = "carbon copy",
bcc = "blind carbon copy",
subject = "subject line",
body = "mail body"
})
```
# Hooks
On-receive mail hook:
```lua
mail.register_on_receive(function(m)
-- "m" is an object in the form: "Mail format"
end)
```
# internal mail format (on-disk)
The mail format on-disk
> (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"
]
}]
```

View File

@ -0,0 +1,27 @@
local invmap = {}
mail.getAttachmentInventory = function(playername)
return invmap[playername]
end
mail.getAttachmentInventoryName = function(playername)
return "mail:" .. playername
end
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
local inv = minetest.create_detached_inventory(mail.getAttachmentInventoryName(name), {})
invmap[name] = inv
end)
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
invmap[name] = nil
if minetest.remove_detached_inventory then
minetest.remove_detached_inventory(mail.getAttachmentInventoryName(name))
end
end)

View File

@ -0,0 +1,6 @@
minetest.register_chatcommand("mail",{
description = "Open the mail interface",
func = function(name)
mail.show_inbox(name)
end
})

694
mods/mail_mod/gui.lua Normal file
View File

@ -0,0 +1,694 @@
-- refactor these to some proper management thing
selected_idxs = {
messages = {},
contacts = {},
to = {},
cc = {},
bcc = {},
}
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)
if playernames == nil then
local length = 0
for k, contact, i, l in mail.pairsByKeys(contacts) do
if i == 1 then length = l end
formspec[#formspec + 1] = ","
formspec[#formspec + 1] = ","
formspec[#formspec + 1] = minetest.formspec_escape(contact.name)
formspec[#formspec + 1] = ","
local note = contact.note
-- display an ellipsis if the note spans multiple lines
local idx = string.find(note, '\n')
if idx ~= nil then
note = string.sub(note, 1, idx-1) .. ' ...'
end
formspec[#formspec + 1] = minetest.formspec_escape(note)
if type(selected) == "string" then
if string.lower(selected) == k then
selected = i
end
end
end
if length > 0 then
if selected and type(selected) == "number" then
formspec[#formspec + 1] = ";"
formspec[#formspec + 1] = tostring(selected + 1)
end
formspec[#formspec + 1] = "]"
else
formspec[#formspec + 1] = "]label[2,4.5;No contacts]"
end
else
if type(playernames) == "string" then
playernames = mail.parse_player_list(playernames)
end
for i,c in ipairs(playernames) do
formspec[#formspec + 1] = ","
formspec[#formspec + 1] = ","
formspec[#formspec + 1] = minetest.formspec_escape(c)
formspec[#formspec + 1] = ","
if contacts[string.lower(c)] == nil then
formspec[#formspec + 1] = ""
else
local note = contacts[string.lower(c)].note
-- display an ellipsis if the note spans multiple lines
local idx = string.find(note, '\n')
if idx ~= nil then
note = string.sub(note, 1, idx-1) .. ' ...'
end
formspec[#formspec + 1] = minetest.formspec_escape(note)
end
if not selected then
if type(selected) == "string" then
if string.lower(selected) == string.lower(c) then
selected = i
end
end
end
end
if #playernames > 0 and selected and type(selected) == "number" then
formspec[#formspec + 1] = ";"
formspec[#formspec + 1] = tostring(selected + 1)
end
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]
button[7.25,0;0.75,0.5;back;X]
label[0,0;From: %s]
label[0,0.4;To: %s]
label[0,0.8;CC: %s]
label[0,1.3;Subject: %s]
textarea[0.25,1.8;8,7.8;body;;%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)
local to = minetest.formspec_escape(message.to)
local cc = minetest.formspec_escape(message.cc)
local subject = minetest.formspec_escape(message.subject)
local body = minetest.formspec_escape(message.body)
formspec = string.format(formspec, from, to, cc, 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 == "" and fields and fields.quit and minetest.get_modpath("unified_inventory") then
unified_inventory.set_inventory_formspec(player, "craft")
end
if formname == "mail:about" then
minetest.after(0.5, function()
mail.show_inbox(player:get_player_name())
end)
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)
return true
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)
return true
elseif fields.new then
mail.show_compose(name)
elseif fields.contacts then
mail.show_contacts(name)
elseif fields.quit then
if minetest.get_modpath("unified_inventory") then
unified_inventory.set_inventory_formspec(player, "craft")
end
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.subject)
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
mail.send({
from = name,
to = fields.to,
cc = fields.cc,
bcc = fields.bcc,
subject = fields.subject,
body = fields.body,
})
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
return true
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
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
elseif fields.mail then
mail.show_inbox(player:get_player_name())
else
return false
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"
mail.read_later_message = mail.read_later_message ..
" or by using the mail button in the inventory"
unified_inventory.register_button("mail", {
type = "image",
image = "mail_button.png",
tooltip = "Mail"
})
end

59
mods/mail_mod/hud.lua Normal file
View File

@ -0,0 +1,59 @@
local huddata = {}
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
local data = {}
data.imageid = player:hud_add({
hud_elem_type = "image",
name = "MailIcon",
position = {x=0.52, y=0.52},
text="",
scale = {x=1,y=1},
alignment = {x=0.5, y=0.5},
})
data.textid = player:hud_add({
hud_elem_type = "text",
name = "MailText",
position = {x=0.55, y=0.52},
text= "",
scale = {x=1,y=1},
alignment = {x=0.5, y=0.5},
})
huddata[name] = data
end)
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
huddata[name] = nil
end)
mail.hud_update = function(playername, messages)
local data = huddata[playername]
local player = minetest.get_player_by_name(playername)
if not data or not player then
return
end
local unreadcount = 0
for _, message in ipairs(messages) do
if message.unread then
unreadcount = unreadcount + 1
end
end
if unreadcount == 0 then
player:hud_change(data.imageid, "text", "")
player:hud_change(data.textid, "text", "")
else
player:hud_change(data.imageid, "text", "email_mail.png")
player:hud_change(data.textid, "text", unreadcount .. " /mail")
end
end

67
mods/mail_mod/init.lua Normal file
View File

@ -0,0 +1,67 @@
mail = {
-- mark webmail fork for other mods
fork = "webmail",
-- api version
apiversion = 1.1,
-- mail directory
maildir = minetest.get_worldpath().."/mails",
contactsdir = minetest.get_worldpath().."/mails/contacts",
-- allow item/node attachments
allow_attachments = minetest.settings:get("mail.allow_attachments") == "true",
webmail = {
-- disallow banned players in the webmail interface
disallow_banned_players = minetest.settings:get("webmail.disallow_banned_players") == "true",
-- url and key to the webmail server
url = minetest.settings:get("webmail.url"),
key = minetest.settings:get("webmail.key")
},
tan = {}
}
local MP = minetest.get_modpath(minetest.get_current_modname())
dofile(MP .. "/util/normalize.lua")
dofile(MP .. "/chatcommands.lua")
dofile(MP .. "/migrate.lua")
dofile(MP .. "/attachment.lua")
dofile(MP .. "/hud.lua")
dofile(MP .. "/storage.lua")
dofile(MP .. "/api.lua")
dofile(MP .. "/gui.lua")
dofile(MP .. "/onjoin.lua")
dofile(MP .. "/node.lua")
-- optional webmail stuff below
local http = QoS and QoS(minetest.request_http_api(), 2) or minetest.request_http_api()
if http then
local webmail_url = mail.webmail.url
local webmail_key = mail.webmail.key
if not webmail_url then error("webmail.url is not defined") end
if not webmail_key then error("webmail.key is not defined") end
print("[mail] loading webmail-component with endpoint: " .. webmail_url)
mail.handlers = {}
dofile(MP .. "/webmail/tan.lua")
dofile(MP .. "/webmail/webmail.lua")
dofile(MP .. "/webmail/hook.lua")
dofile(MP .. "/webmail/handler_auth.lua")
dofile(MP .. "/webmail/handler_send.lua")
dofile(MP .. "/webmail/handler_messages.lua")
dofile(MP .. "/webmail/handler_delete.lua")
dofile(MP .. "/webmail/handler_mark_read.lua")
dofile(MP .. "/webmail/handler_mark_unread.lua")
mail.webmail_init(http, webmail_url, webmail_key)
end
-- migrate storage
mail.migrate()

BIN
mods/mail_mod/mailbox.blend Normal file

Binary file not shown.

Binary file not shown.

50
mods/mail_mod/migrate.lua Normal file
View File

@ -0,0 +1,50 @@
-- migrate from mail.db to player-file-based mailbox
mail.migrate = function()
-- 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")
end
end
mail.migrate_contacts = function(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?
end
end

3
mods/mail_mod/mod.conf Normal file
View File

@ -0,0 +1,3 @@
name = mail
description = mail mod
optional_depends = unified_inventory,default,xban2,qos

View File

@ -0,0 +1,143 @@
# Blender v2.91.2 OBJ File: 'mailbox.blend'
# www.blender.org
o Cube.001_Cube
v -0.375000 -0.500000 0.437500
v 0.375000 0.500000 -0.250000
v 0.375000 -0.500000 0.437500
v 0.375000 0.500000 0.375000
v -0.375000 -0.500000 -0.312500
v -0.375000 0.500000 -0.250000
v 0.375000 -0.500000 -0.312500
v -0.375000 0.500000 0.375000
v 0.375000 -0.312500 -0.312500
v 0.375000 0.437500 -0.312500
v -0.375000 0.437500 -0.312500
v -0.375000 -0.312500 -0.312500
v 0.375000 0.437500 0.437500
v 0.375000 -0.312500 0.437500
v -0.375000 -0.312500 0.437500
v -0.375000 0.437500 0.437500
v -0.437500 0.500000 0.375000
v -0.437500 -0.312500 0.375000
v -0.437500 -0.312500 -0.250000
v -0.437500 0.500000 -0.250000
v -0.437500 0.437500 -0.312500
v -0.437500 -0.312500 -0.312500
v -0.437500 0.437500 0.437500
v -0.437500 -0.312500 0.437500
v 0.437500 -0.312500 0.375000
v 0.437500 -0.312500 -0.250000
v 0.437500 0.500000 -0.250000
v 0.437500 0.500000 0.375000
v 0.437500 -0.312500 -0.312500
v 0.437500 0.437500 -0.312500
v 0.437500 0.437500 0.437500
v 0.437500 -0.312500 0.437500
v -0.437500 -0.500000 0.375000
v -0.437500 -0.500000 -0.250000
v -0.437500 -0.500000 -0.312500
v -0.437500 -0.500000 0.437500
v 0.437500 -0.500000 0.375000
v 0.437500 -0.500000 -0.250000
v 0.437500 -0.500000 -0.312500
v 0.437500 -0.500000 0.437500
vt 0.343750 0.968750
vt 0.656250 0.968750
vt 0.656250 1.000000
vt 0.343750 1.000000
vt 0.968750 0.500000
vt 0.968750 0.468750
vt 1.000000 0.468750
vt 1.000000 0.500000
vt 0.593750 0.500000
vt 0.593750 0.468750
vt 0.656250 0.593750
vt 0.625000 0.593750
vt 0.625000 0.500000
vt 0.656250 0.500000
vt 0.656250 0.593750
vt 0.343750 0.593750
vt 0.968750 0.093750
vt 0.593750 0.093750
vt 0.343750 0.562500
vt 0.656250 0.562500
vt 1.000000 0.593750
vt 0.968750 0.593750
vt 0.968750 0.500000
vt 1.000000 0.500000
vt 0.562500 0.093750
vt 0.562500 0.000000
vt 0.593750 0.000000
vt 0.406250 0.093750
vt 0.406250 0.468750
vt 0.031250 0.468750
vt 0.031250 0.093750
vt 0.437500 0.093750
vt 0.406250 0.000000
vt 0.437500 0.000000
vt 0.406250 0.500000
vt 0.031250 0.500000
vt 0.000000 0.093750
vt 0.000000 0.000000
vt 0.031250 0.000000
vt 0.968750 1.000000
vt 1.000000 0.968750
vt 0.656250 1.000000
vt 0.625000 0.968750
vt 1.000000 0.093750
vt 0.375000 0.593750
vt 0.343750 0.593750
vt 0.343750 0.500000
vt 0.375000 0.500000
vt 0.968750 0.000000
vt 1.000000 0.000000
vt 0.000000 0.500000
vt 0.000000 0.468750
vt 0.031250 1.000000
vt 0.031250 0.593750
vt 0.375000 0.968750
vt 0.000000 0.968750
vt 0.000000 0.593750
vt 0.000000 0.500000
vt 0.031250 0.500000
vt 0.437500 0.468750
vt 0.437500 0.500000
vt 0.562500 0.500000
vt 0.562500 0.468750
vn 0.0000 1.0000 0.0000
vn 0.0000 0.7071 -0.7071
vn -1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
vn 0.0000 0.0000 1.0000
vn 0.0000 0.7071 0.7071
vn 1.0000 0.0000 0.0000
s off
f 2/1/1 4/2/1 28/3/1 27/4/1
f 6/5/2 11/6/2 21/7/2 20/8/2
f 6/5/2 2/9/2 10/10/2 11/6/2
f 19/11/3 22/12/3 35/13/3 34/14/3
f 8/15/1 4/2/1 2/1/1 6/16/1
f 12/17/4 11/6/4 10/10/4 9/18/4
f 8/15/1 6/16/1 20/19/1 17/20/1
f 24/21/3 18/22/3 33/23/3 36/24/3
f 9/18/4 29/25/4 39/26/4 7/27/4
f 14/28/5 13/29/5 16/30/5 15/31/5
f 32/32/5 14/28/5 3/33/5 40/34/5
f 4/35/6 8/36/6 16/30/6 13/29/6
f 15/31/5 24/37/5 36/38/5 1/39/5
f 17/40/3 18/22/3 24/21/3 23/41/3
f 18/22/3 17/40/3 20/42/3 19/11/3
f 19/11/3 20/42/3 21/43/3 22/12/3
f 11/6/4 12/17/4 22/44/4 21/7/4
f 29/45/7 26/46/7 38/47/7 39/48/7
f 22/44/4 12/17/4 5/49/4 35/50/4
f 16/30/6 8/36/6 17/51/6 23/52/6
f 15/31/5 16/30/5 23/52/5 24/37/5
f 26/46/7 27/4/7 28/53/7 25/54/7
f 27/4/7 26/46/7 29/45/7 30/55/7
f 25/54/7 28/53/7 31/56/7 32/57/7
f 25/54/7 32/57/7 40/58/7 37/59/7
f 13/29/5 14/28/5 32/32/5 31/60/5
f 4/35/6 13/29/6 31/60/6 28/61/6
f 10/10/2 2/9/2 27/62/2 30/63/2
f 9/18/4 10/10/4 30/63/4 29/25/4

14
mods/mail_mod/node.lua Normal file
View File

@ -0,0 +1,14 @@
minetest.register_node('mail:mailbox', {
description = 'Mail Box',
drawtype = 'mesh',
mesh = 'mail_mailbox.obj',
tiles = {'mail_mailbox_uv.png'},
paramtype = 'light',
paramtype2 = 'facedir',
sunlight_propagates = true,
groups = {breakable=1},
on_rightclick = function(pos, node, clicker)
local name = clicker:get_player_name()
mail.show_inbox(name)
end
})

21
mods/mail_mod/onjoin.lua Normal file
View File

@ -0,0 +1,21 @@
minetest.register_on_joinplayer(function(player)
minetest.after(2, function(name)
local messages = mail.getMessages(name)
local unreadcount = 0
for _, message in pairs(messages) do
if message.unread then
unreadcount = unreadcount + 1
end
end
if unreadcount > 0 then
minetest.chat_send_player(name,
minetest.colorize("#00f529", "(" .. unreadcount .. ") You have mail! Type /mail to read"))
end
end, player:get_player_name())
mail.migrate_contacts(player:get_player_name())
end)

87
mods/mail_mod/storage.lua Normal file
View File

@ -0,0 +1,87 @@
-- TODO: maybe local cache?
function mail.getMailFile(playername)
local saneplayername = string.gsub(playername, "[.|/]", "")
return mail.maildir .. "/" .. saneplayername .. ".json"
end
function mail.getContactsFile(playername)
local saneplayername = string.gsub(playername, "[.|/]", "")
return mail.maildir .. "/contacts/" .. saneplayername .. ".json"
end
mail.getMessages = function(playername)
local messages = mail.read_json_file(mail.getMailFile(playername))
if messages then
mail.hud_update(playername, messages)
end
return messages
end
mail.setMessages = function(playername, messages)
if mail.write_json_file(mail.getMailFile(playername), messages) then
mail.hud_update(playername, messages)
return true
else
minetest.log("error","[mail] Save failed - messages may be lost! ("..playername..")")
return false
end
end
mail.getContacts = function(playername)
return mail.read_json_file(mail.getContactsFile(playername))
end
function mail.pairsByKeys(t, f)
-- http://www.lua.org/pil/19.3.html
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = 0 -- iterator variable
local iter = function() -- iterator function
i = i + 1
if a[i] == nil then
return nil
else
--return a[i], t[a[i]]
-- add the current position and the length for convenience
return a[i], t[a[i]], i, #a
end
end
return iter
end
mail.setContacts = function(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

Binary file not shown.

View File

@ -0,0 +1,4 @@
User,Password
-----------------
test,enter
test2,enter

View File

@ -0,0 +1,79 @@
#!/bin/bash
MINETEST_VERSION=5.2.0
# prerequisites
jq --version || exit 1
curl --version || exit 1
# ensure proper current directory
CWD=$(dirname $0)
cd ${CWD}
# setup
unset use_proxy
unset http_proxy
unset https_proxy
unset HTTP_PROXY
unset HTTPS_PROXY
# run mail-server
docker pull minetestmail/mail
docker run --name mail --rm \
-e WEBMAILKEY=myserverkey \
-e WEBMAIL_DEBUG=true \
--network host \
minetestmail/mail &
# wait for startup
bash -c 'while !</dev/tcp/localhost/8080; do sleep 1; done;'
# start minetest with mail mod
docker pull registry.gitlab.com/minetest/minetest/server:${MINETEST_VERSION}
docker run --rm --name minetest \
-u root:root \
-v $(pwd)/minetest.conf:/etc/minetest/minetest.conf:ro \
-v $(pwd)/world.mt:/root/.minetest/worlds/world/world.mt \
-v $(pwd)/auth.sqlite:/root/.minetest/worlds/world/auth.sqlite \
-v $(pwd)/../:/root/.minetest/worlds/world/worldmods/mail \
-v $(pwd)/test_mod:/root/.minetest/worlds/world/worldmods/mail_test \
-e use_proxy=false \
-e http_proxy= \
-e HTTP_PROXY= \
--network host \
registry.gitlab.com/minetest/minetest/server:${MINETEST_VERSION} &
# prepare cleanup
function cleanup {
# cleanup
docker stop mail
docker stop minetest
}
trap cleanup EXIT
# wait for startup
sleep 5
# Execute calls against mail-server
# login
LOGIN_DATA='{"username":"test","password":"enter"}'
RES=$(curl --data "${LOGIN_DATA}" -H "Content-Type: application/json" "http://127.0.0.1:8080/api/login")
echo Login response: $RES
SUCCESS=$(echo $RES | jq -r .success)
TOKEN=$(echo $RES | jq -r .token)
# login succeeded
test "$SUCCESS" == "true" || exit 1
# token extracted
test -n "$TOKEN" || exit 1
# fetch mails
RES=$(curl -H "Authorization: ${TOKEN}" "http://127.0.0.1:8080/api/inbox")
echo Mailbox: ${RES}
# inbox count is 1
test "$(echo $RES | jq '. | length')" == "1" || exit 1
echo "Test complete!"

View File

@ -0,0 +1,4 @@
secure.http_mods = mail
webmail.url = http://localhost:8080
webmail.key = myserverkey
webmail.disallow_banned_players = true

10
mods/mail_mod/test/start.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
docker run --rm -it \
-u root:root \
-v $(pwd)/minetest.conf:/etc/minetest/minetest.conf \
-v $(pwd)/auth.sqlite:/root/.minetest/worlds/world/auth.sqlite \
-v $(pwd)/../:/root/.minetest/worlds/world/worldmods/mail_mod \
-v mail_world:/root/.minetest/worlds/world/ \
--network host \
registry.gitlab.com/minetest/minetest/server:5.2.0

View File

@ -0,0 +1,7 @@
minetest.log("warning", "[TEST] integration-test enabled!")
minetest.register_on_mods_loaded(function()
minetest.log("warning", "[TEST] starting tests")
mail.send("spammer", "test", "subject", "body");
end)

View File

@ -0,0 +1,2 @@
name = mail_test
depends = mail

View File

@ -0,0 +1,6 @@
enable_damage = true
creative_mode = false
gameid = minetest
auth_backend = sqlite3
backend = sqlite3
player_backend = sqlite3

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -0,0 +1,91 @@
-- 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

@ -0,0 +1,60 @@
--[[
return the field normalized (comma separated, single space)
and add individual player names to recipient list
--]]
function mail.normalize_players_and_add_recipients(field, recipients)
local order = mail.parse_player_list(field)
for _, c in ipairs(order) do
if recipients[string.lower(c)] == nil then
recipients[string.lower(c)] = c
end
end
return mail.concat_player_list(order)
end
function mail.parse_player_list(field)
if not field then
return {}
end
local separator = ", "
local pattern = "([^" .. separator .. "]+)"
-- get individual players
local player_set = {}
local order = {}
field:gsub(pattern, function(c)
if player_set[string.lower(c)] == nil then
player_set[string.lower(c)] = c
order[#order+1] = c
end
end)
return order
end
function mail.concat_player_list(order)
-- turn list of players back into normalized string
return table.concat(order, ", ")
end
function mail.player_in_list(name, list)
list = list or {}
if type(list) == "string" then
list = mail.parse_player_list(list)
end
for _, c in pairs(list) do
if name == c then
return true
end
end
return false
end
function mail.ensure_new_format(message, name)
if message.to == nil then
message.to = name
end
end

View File

@ -0,0 +1,45 @@
local has_xban2_mod = minetest.get_modpath("xban2")
-- auth request from webmail
function mail.handlers.auth(auth)
local handler = minetest.get_auth_handler()
minetest.log("action", "[webmail] auth: " .. auth.name)
local success = false
local banned = false
local message = ""
if mail.webmail.disallow_banned_players and has_xban2_mod then
-- check xban db
local xbanentry = xban.find_entry(auth.name)
if xbanentry and xbanentry.banned then
banned = true
message = "Banned!"
end
end
if not banned then
-- check tan
local tan = mail.tan[auth.name]
if tan ~= nil then
success = tan == auth.password
end
-- check auth
if not success then
local entry = handler.get_auth(auth.name)
if entry and minetest.check_password_entry(auth.name, entry.password, auth.password) then
success = true
end
end
end
mail.channel.send({
type = "auth",
data = {
name = auth.name,
success = success,
message = message
}
})
end

View File

@ -0,0 +1,8 @@
-- remove mail
function mail.handlers.delete(playername, index)
local messages = mail.getMessages(playername)
if messages[index] then
table.remove(messages, index)
end
mail.setMessages(playername, messages)
end

View File

@ -0,0 +1,9 @@
-- mark mail as read
function mail.handlers.mark_read(playername, index)
local messages = mail.getMessages(playername)
if messages[index] then
messages[index].unread = false
end
mail.setMessages(playername, messages)
end

View File

@ -0,0 +1,9 @@
-- mark mail as unread
function mail.handlers.mark_unread(playername, index)
local messages = mail.getMessages(playername)
if messages[index] then
messages[index].unread = true
end
mail.setMessages(playername, messages)
end

View File

@ -0,0 +1,9 @@
-- get player messages request from webmail
function mail.handlers.messages(playername)
local messages = mail.getMessages(playername)
mail.channel.send({
type = "player-messages",
playername = playername,
data = messages
})
end

View File

@ -0,0 +1,7 @@
-- send request from webmail
function mail.handlers.send(sendmail)
-- send mail from webclient
minetest.log("action", "[webmail] sending mail from webclient: " .. sendmail.from .. " -> " .. sendmail.to)
mail.send(sendmail)
end

View File

@ -0,0 +1,8 @@
function mail.webmail_send_hook(m)
mail.channel.send({
type = "new-message",
data = m
})
end
mail.register_on_receive(mail.webmail_send_hook)

View File

@ -0,0 +1,16 @@
minetest.register_chatcommand("webmail_tan", {
description = "generates a tan (temporary access number) for the webmail access",
func = function(name)
local tan = "" .. math.random(1000, 9999)
mail.tan[name] = tan
return true, "Your tan is " .. tan .. ", it will expire upon leaving the game"
end
})
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
mail.tan[name] = nil
end)

View File

@ -0,0 +1,31 @@
local MP = minetest.get_modpath(minetest.get_current_modname())
local Channel = dofile(MP .. "/util/channel.lua")
function mail.webmail_init(http, url, key)
mail.channel = Channel(http, url .. "/api/minetest/channel", {
extra_headers = { "webmailkey: " .. key }
})
mail.channel.receive(function(data)
if data.type == "auth" then
mail.handlers.auth(data.data)
elseif data.type == "send" then
mail.handlers.send(data.data) -- { src, dst, subject, body }
elseif data.type == "delete-mail" then
mail.handlers.delete(data.playername, data.index) -- index 1-based
elseif data.type == "mark-mail-read" then
mail.handlers.mark_read(data.playername, data.index) -- index 1-based
elseif data.type == "mark-mail-unread" then
mail.handlers.mark_unread(data.playername, data.index) -- index 1-based
elseif data.type == "player-messages" then
mail.handlers.messages(data.data)
end
end)
end