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