374 lines
16 KiB
Lua
374 lines
16 KiB
Lua
|
|
-- ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
-- Mail handlers
|
|||
|
|
-- ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
Mail = {}
|
|||
|
|
|
|||
|
|
-- Rate limiting: identifier -> { count, resetAt }
|
|||
|
|
local _rateMap = {}
|
|||
|
|
|
|||
|
|
--- Darf diese Person das Postfach benutzen?
|
|||
|
|
--- '' bedeutet "persoenliches Postfach" und ist immer erlaubt. Ohne diese
|
|||
|
|
--- Pruefung koennte der Client jede beliebige Adresse mitschicken und damit in
|
|||
|
|
--- fremden Ordnern lesen und schreiben.
|
|||
|
|
---@return string die gepruefte Adresse, oder '' bei fehlendem Zugriff
|
|||
|
|
local function CheckMailbox(identifier, address)
|
|||
|
|
address = tostring(address or '')
|
|||
|
|
if address == '' then return '' end
|
|||
|
|
if GetResourceState('ic-mail') ~= 'started' then return '' end
|
|||
|
|
|
|||
|
|
local ok = exports['ic-mail']:CanAccessAddress(identifier, address)
|
|||
|
|
return ok and address or ''
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
--- Postfachliste samt Signaturen an einen Spieler schicken.
|
|||
|
|
--- Steht hier als Funktion, weil sie aus mehreren Ereignissen gebraucht wird –
|
|||
|
|
--- ein TriggerEvent auf das Netzwerkereignis waere kein Ersatz: dort ist
|
|||
|
|
--- `source` nicht der Spieler, und der Aufruf liefe ins Leere.
|
|||
|
|
local function SendMailboxes(src, identifier)
|
|||
|
|
local mailboxes = {}
|
|||
|
|
local personalAddr = ''
|
|||
|
|
|
|||
|
|
if GetResourceState('ic-mail') == 'started' then
|
|||
|
|
personalAddr = exports['ic-mail']:GetPrimaryAddress(identifier) or ''
|
|||
|
|
local addrs = exports['ic-mail']:GetAddressesForIdentifier(identifier) or {}
|
|||
|
|
table.sort(addrs, function(a, b)
|
|||
|
|
if a.type == 'personal' and b.type ~= 'personal' then return true end
|
|||
|
|
if a.type ~= 'personal' and b.type == 'personal' then return false end
|
|||
|
|
return (a.address or '') < (b.address or '')
|
|||
|
|
end)
|
|||
|
|
mailboxes = addrs
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local personalSig = ''
|
|||
|
|
for _, mb in ipairs(mailboxes) do
|
|||
|
|
if mb.address == personalAddr then personalSig = mb.signature or '' end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
TriggerClientEvent('pc-live:client:sharedMailboxes', src, mailboxes, personalAddr, personalSig)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local function CheckRateLimit(identifier)
|
|||
|
|
local now = os.time()
|
|||
|
|
local entry = _rateMap[identifier]
|
|||
|
|
if not entry or now >= entry.resetAt then
|
|||
|
|
_rateMap[identifier] = { count = 1, resetAt = now + 60 }
|
|||
|
|
return true
|
|||
|
|
end
|
|||
|
|
if entry.count >= Config.MailRateLimit then
|
|||
|
|
return false
|
|||
|
|
end
|
|||
|
|
entry.count = entry.count + 1
|
|||
|
|
return true
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- ── Fetch inbox ─────────────────────────────────────────────────────────────
|
|||
|
|
RegisterNetEvent('pc-live:server:getInbox', function()
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
|
|||
|
|
local rows = DB.GetInboxOnly(user.identifier)
|
|||
|
|
local folders = DB.GetFolders(user.identifier, '')
|
|||
|
|
TriggerClientEvent('pc-live:client:inboxData', src, rows, folders)
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Fetch sent ───────────────────────────────────────────────────────────────
|
|||
|
|
RegisterNetEvent('pc-live:server:getSent', function()
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
|
|||
|
|
local rows = DB.GetSent(user.identifier)
|
|||
|
|
TriggerClientEvent('pc-live:client:sentData', src, rows)
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Fetch folder mails ───────────────────────────────────────────────────────
|
|||
|
|
RegisterNetEvent('pc-live:server:getFolderMails', function(folderId, address)
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
if type(folderId) ~= "number" then return end
|
|||
|
|
|
|||
|
|
local box = CheckMailbox(user.identifier, address)
|
|||
|
|
if not DB.GetFolder(folderId, user.identifier, box) then return end
|
|||
|
|
|
|||
|
|
local rows = DB.GetFolderMails(folderId, user.identifier)
|
|||
|
|
TriggerClientEvent('pc-live:client:folderMailsData', src, folderId, rows)
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Ordner eines Postfachs holen ─────────────────────────────────────────────
|
|||
|
|
-- Wird beim Wechsel des Postfachs gerufen: jedes Postfach hat eigene Ordner.
|
|||
|
|
RegisterNetEvent('pc-live:server:getFolders', function(address)
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
|
|||
|
|
local box = CheckMailbox(user.identifier, address)
|
|||
|
|
TriggerClientEvent('pc-live:client:foldersData', src, box,
|
|||
|
|
DB.GetFolders(user.identifier, box))
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Create folder ────────────────────────────────────────────────────────────
|
|||
|
|
RegisterNetEvent('pc-live:server:createFolder', function(name, address)
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
name = Utils.Sanitize(tostring(name or "")):sub(1, 60)
|
|||
|
|
if #name < 1 then return end
|
|||
|
|
|
|||
|
|
local box = CheckMailbox(user.identifier, address)
|
|||
|
|
local id = DB.CreateFolder(user.identifier, box, name)
|
|||
|
|
|
|||
|
|
TriggerClientEvent('pc-live:client:folderCreated', src, { id = id, name = name, address = box })
|
|||
|
|
|
|||
|
|
-- Im geteilten Postfach sehen die anderen den Ordner sofort.
|
|||
|
|
if box ~= '' then
|
|||
|
|
for playerSrc, session in pairs(ActiveSessions) do
|
|||
|
|
if playerSrc ~= src and CheckMailbox(session.identifier, box) ~= '' then
|
|||
|
|
TriggerClientEvent('pc-live:client:foldersData', playerSrc, box,
|
|||
|
|
DB.GetFolders(session.identifier, box))
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Delete folder ────────────────────────────────────────────────────────────
|
|||
|
|
RegisterNetEvent('pc-live:server:deleteFolder', function(folderId, address)
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
if type(folderId) ~= "number" then return end
|
|||
|
|
|
|||
|
|
local box = CheckMailbox(user.identifier, address)
|
|||
|
|
if not DB.GetFolder(folderId, user.identifier, box) then return end
|
|||
|
|
|
|||
|
|
DB.DeleteFolder(folderId, user.identifier, box)
|
|||
|
|
TriggerClientEvent('pc-live:client:folderDeleted', src, folderId, box)
|
|||
|
|
|
|||
|
|
if box ~= '' then
|
|||
|
|
for playerSrc, session in pairs(ActiveSessions) do
|
|||
|
|
if playerSrc ~= src and CheckMailbox(session.identifier, box) ~= '' then
|
|||
|
|
TriggerClientEvent('pc-live:client:folderDeleted', playerSrc, folderId, box)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Move mail to folder ──────────────────────────────────────────────────────
|
|||
|
|
RegisterNetEvent('pc-live:server:moveMail', function(mailId, folderId, address)
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
if type(mailId) ~= "number" then return end
|
|||
|
|
-- folderId may be nil (move to inbox)
|
|||
|
|
if folderId ~= nil and type(folderId) ~= "number" then return end
|
|||
|
|
|
|||
|
|
local box = CheckMailbox(user.identifier, address)
|
|||
|
|
if folderId ~= nil and not DB.GetFolder(folderId, user.identifier, box) then return end
|
|||
|
|
|
|||
|
|
DB.MoveMailToFolder(mailId, folderId, user.identifier, box)
|
|||
|
|
TriggerClientEvent('pc-live:client:mailMoved', src, mailId, folderId)
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Read mail ───────────────────────────────────────────────────────────────
|
|||
|
|
RegisterNetEvent('pc-live:server:readMail', function(mailId)
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
if type(mailId) ~= "number" then return end
|
|||
|
|
|
|||
|
|
local mail = DB.GetMail(mailId, user.identifier)
|
|||
|
|
if not mail then return end
|
|||
|
|
|
|||
|
|
DB.MarkRead(mailId, user.identifier)
|
|||
|
|
mail.is_read = 1
|
|||
|
|
TriggerClientEvent('pc-live:client:mailContent', src, mail)
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Send mail ────────────────────────────────────────────────────────────────
|
|||
|
|
RegisterNetEvent('pc-live:server:sendMail', function(payload)
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
|
|||
|
|
-- Validate payload shape
|
|||
|
|
if type(payload) ~= "table" then return end
|
|||
|
|
|
|||
|
|
local toId = Utils.Sanitize(tostring(payload.to or "")):sub(1, 120)
|
|||
|
|
local subject = Utils.Sanitize(tostring(payload.subject or "")):sub(1, Config.MailMaxSubject)
|
|||
|
|
local body = Utils.Sanitize(tostring(payload.body or "")):sub(1, Config.MailMaxBody)
|
|||
|
|
|
|||
|
|
if #toId < 3 or #subject < 1 or #body < 1 then
|
|||
|
|
Bridge.Notify(src, "Invalid mail data.", "error")
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Rate limit
|
|||
|
|
if not CheckRateLimit(user.identifier) then
|
|||
|
|
Bridge.Notify(src, "You are sending mails too fast. Please wait.", "error")
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Absender-Adresse ermitteln (ic-mail optional)
|
|||
|
|
local fromDisplay = user.identifier
|
|||
|
|
if GetResourceState and GetResourceState('ic-mail') == 'started' then
|
|||
|
|
local addr = exports['ic-mail']:GetPrimaryAddress(user.identifier)
|
|||
|
|
if addr then fromDisplay = addr end
|
|||
|
|
|
|||
|
|
-- Versand aus geteiltem Postfach: Zugriff prüfen
|
|||
|
|
local fromMailbox = Utils.Sanitize(tostring(payload.fromMailbox or '')):sub(1, 120)
|
|||
|
|
if fromMailbox ~= '' and fromMailbox ~= fromDisplay then
|
|||
|
|
local addrs = exports['ic-mail']:GetAddressesForIdentifier(user.identifier) or {}
|
|||
|
|
for _, a in ipairs(addrs) do
|
|||
|
|
if a.address == fromMailbox then
|
|||
|
|
fromDisplay = fromMailbox
|
|||
|
|
break
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Empfänger auflösen: @-Adresse → Identifier-Liste, sonst direkt
|
|||
|
|
local recipients = {}
|
|||
|
|
if toId:find('@') and GetResourceState and GetResourceState('ic-mail') == 'started' then
|
|||
|
|
local resolved = exports['ic-mail']:ResolveAddress(toId)
|
|||
|
|
if resolved and resolved.identifiers and #resolved.identifiers > 0 then
|
|||
|
|
recipients = resolved.identifiers
|
|||
|
|
else
|
|||
|
|
Bridge.Notify(src, "Empfänger-Adresse nicht gefunden.", "error")
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
recipients = { toId }
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- Eine Kennung fuer diese Zustellung. Alle Kopien tragen sie, damit das
|
|||
|
|
-- Einsortieren in einen Ordner im geteilten Postfach fuer alle gilt.
|
|||
|
|
local deliveryId = ('%08x-%04x-%04x-%04x-%012x'):format(
|
|||
|
|
os.time(), math.random(0, 0xFFFF), math.random(0, 0xFFFF),
|
|||
|
|
math.random(0, 0xFFFF), math.random(0, 0xFFFFFFFFFFFF))
|
|||
|
|
|
|||
|
|
-- Mail an alle Empfänger senden (pc_live_mail + ic-mail Dual-Write)
|
|||
|
|
for _, recipId in ipairs(recipients) do
|
|||
|
|
DB.SendMailExtended(recipId, fromDisplay, user.identifier, toId, subject, body, deliveryId)
|
|||
|
|
-- ic-mail parallel benachrichtigen (zentrale Mailplattform)
|
|||
|
|
pcall(function()
|
|||
|
|
if GetResourceState('ic-mail') == 'started' then
|
|||
|
|
exports['ic-mail']:SendSystemMail(recipId, subject, body)
|
|||
|
|
end
|
|||
|
|
end)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
Bridge.Notify(src, "Mail gesendet.", "success")
|
|||
|
|
TriggerClientEvent('pc-live:client:mailSent', src)
|
|||
|
|
|
|||
|
|
-- Online-Empfänger benachrichtigen
|
|||
|
|
for playerSrc, session in pairs(ActiveSessions) do
|
|||
|
|
for _, recipId in ipairs(recipients) do
|
|||
|
|
if session.identifier == recipId then
|
|||
|
|
TriggerClientEvent('pc-live:client:newMailNotify', playerSrc)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Delete mail ─────────────────────────────────────────────────────────────
|
|||
|
|
RegisterNetEvent('pc-live:server:deleteMail', function(mailId)
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
if type(mailId) ~= "number" then return end
|
|||
|
|
|
|||
|
|
DB.DeleteMail(mailId, user.identifier)
|
|||
|
|
TriggerClientEvent('pc-live:client:mailDeleted', src, mailId)
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Geteilte Postfächer (ic-mail Integration) ────────────────────────────────
|
|||
|
|
RegisterNetEvent('pc-live:server:getSharedMailboxes', function()
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
|
|||
|
|
SendMailboxes(src, user.identifier)
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Mail-Adresse prüfen (Ersteinrichtung) ────────────────────────────────────
|
|||
|
|
|
|||
|
|
RegisterNetEvent('pc-live:server:checkMailAddress', function()
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
|
|||
|
|
if GetResourceState('ic-mail') ~= 'started' then return end
|
|||
|
|
|
|||
|
|
local existing = exports['ic-mail']:GetPrimaryAddress(user.identifier)
|
|||
|
|
if existing then
|
|||
|
|
-- Adresse vorhanden → Status senden, Postfach lädt der Client selbst nach
|
|||
|
|
TriggerClientEvent('pc-live:client:mailAddressStatus', src, true, existing, nil)
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local realms = exports['ic-mail']:GetRealms() or {}
|
|||
|
|
local citizenRealms = {}
|
|||
|
|
for _, r in ipairs(realms) do
|
|||
|
|
if r.realm_type == 'citizen' and r.active then
|
|||
|
|
table.insert(citizenRealms, r)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
TriggerClientEvent('pc-live:client:mailAddressStatus', src, false, nil, citizenRealms)
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
RegisterNetEvent('pc-live:server:createMailAddress', function(username)
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
if type(username) ~= 'string' or #username < 2 or #username > 40 then
|
|||
|
|
TriggerClientEvent('pc-live:client:mailCreateResult', src, false, 'Benutzername muss 2-40 Zeichen haben')
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
username = username:lower():gsub('[^a-z0-9._%-]', ''):gsub('^%.+', ''):gsub('%.+$', '')
|
|||
|
|
if #username < 2 then
|
|||
|
|
TriggerClientEvent('pc-live:client:mailCreateResult', src, false, 'Ungültiger Benutzername')
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
if GetResourceState('ic-mail') ~= 'started' then
|
|||
|
|
TriggerClientEvent('pc-live:client:mailCreateResult', src, false, 'Mail-System nicht verfügbar')
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
local existing = exports['ic-mail']:GetPrimaryAddress(user.identifier)
|
|||
|
|
if existing then
|
|||
|
|
TriggerClientEvent('pc-live:client:mailCreateResult', src, true, nil, existing)
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
local addr = exports['ic-mail']:EnsurePersonalAddress(user.identifier, username)
|
|||
|
|
if addr then
|
|||
|
|
TriggerClientEvent('pc-live:client:mailCreateResult', src, true, nil, addr)
|
|||
|
|
else
|
|||
|
|
TriggerClientEvent('pc-live:client:mailCreateResult', src, false, 'Benutzername bereits vergeben')
|
|||
|
|
end
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- ── Signaturen (ic-mail) ─────────────────────────────────────────────────────
|
|||
|
|
-- Die Signatur gehoert zum Postfach: eine Firmenadresse traegt dieselbe
|
|||
|
|
-- Fusszeile, egal wer sie gerade bedient. Geprueft wird in ic-mail.
|
|||
|
|
|
|||
|
|
RegisterNetEvent('pc-live:server:setSignature', function(address, signature)
|
|||
|
|
local src = source
|
|||
|
|
local user = ActiveSessions[src]
|
|||
|
|
if not user then return end
|
|||
|
|
|
|||
|
|
if GetResourceState('ic-mail') ~= 'started' then
|
|||
|
|
TriggerClientEvent('pc-live:client:signatureSaved', src, false, 'Mailsystem nicht verfügbar.')
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local ok, err = exports['ic-mail']:SetSignature(
|
|||
|
|
tostring(address or ''), user.identifier, tostring(signature or ''))
|
|||
|
|
|
|||
|
|
TriggerClientEvent('pc-live:client:signatureSaved', src, ok == true, err)
|
|||
|
|
|
|||
|
|
-- Postfachliste nachschieben, damit die neue Signatur sofort greift.
|
|||
|
|
if ok then SendMailboxes(src, user.identifier) end
|
|||
|
|
end)
|