--[[ *** DataStore_Mails *** Written by : Thaoky, EU-Marécages de Zangar July 16th, 2009 --]] if not DataStore then return end local addonName = "DataStore_Mails" _G[addonName] = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0", "AceComm-3.0", "AceSerializer-3.0", "AceTimer-3.0") local addon = _G[addonName] local THIS_ACCOUNT = "Default" local commPrefix = "DS_Mails" -- let's keep it a bit shorter than the addon name, this goes on a comm channel, a byte is a byte ffs :p local MAIL_EXPIRY = 30 -- Mails expire after 30 days -- Func Call Spam Protection local FCSP_timer_OnBagUpdate -- Message types local MSG_SENDMAIL_INIT = 1 local MSG_SENDMAIL_END = 2 local MSG_SENDMAIL_ATTACHMENT = 3 local MSG_SENDMAIL_BODY = 4 local ICON_COIN = "Interface\\Icons\\INV_Misc_Coin_01" local ICON_NOTE = "Interface\\Icons\\INV_Misc_Note_01" local AddonDB_Defaults = { global = { Options = { ScanMailBody = 1, -- by default, scan the body of a mail (this action marks it as read) CheckMailExpiry = 1, -- check mail expiry or not MailWarningThreshold = 5, CheckMailExpiryAllAccounts = 1, CheckMailExpiryAllRealms = 1, }, Characters = { ['*'] = { -- ["Account.Realm.Name"] lastUpdate = nil, -- last time the mail was checked for this char lastVisitDate = nil, -- in YYYY MM DD hh:mm, for external apps Mails = { ['*'] = { icon = nil, link = nil, count = nil, money = nil, lastCheck = 0, -- last time "THIS" mail was checked (can be different than that of the mailbox) text = nil, subject = nil, sender = nil, daysLeft = 0, returned = nil, } }, MailCache = { -- same structure as "Mail", but serves as a cache for mails sent by a guildmate, until the mail actually arrives in the real mailbox (1h delay) ['*'] = { icon = nil, link = nil, count = nil, money = nil, lastCheck = 0, -- last time "THIS" mail was checked (can be different than that of the mailbox) text = nil, subject = nil, sender = nil, daysLeft = 0, } }, } } } } -- *** Utility functions *** local function GetIDFromLink(link) return tonumber(link:match("item:(%d+)")) end local function GetOption(option) return addon.db.global.Options[option] end local function GetMailTable(character, index) -- depending on the index passed, returns the right mail entry either from the "Mails" table or the "MailCache" table -- The assumption is that the MailCache entries come after the Mails entries -- This function is pure utility, and is not made public to client addons if index <= #character.Mails then return character.Mails[index] else index = index - #character.Mails return character.MailCache[index] end end local function GuildWhisper(player, messageType, ...) if DataStore:IsGuildMemberOnline(player) then local serializedData = addon:Serialize(messageType, ...) addon:SendCommMessage(commPrefix, serializedData, "WHISPER", player) end end local function ReadMailAttachments(mailIndex) -- reads the attachments of a mail that is about to be sent or returned wipe(addon.Attachments) local name, icon, count, link for attachmentIndex = 1, 12 do if mailIndex then -- returned mail name, icon, count = GetInboxItem(mailIndex, attachmentIndex) link = GetInboxItemLink(mailIndex, attachmentIndex) else -- if there is no index, it's a sent mail name, icon, count = GetSendMailItem(attachmentIndex) link = GetSendMailItemLink(attachmentIndex) end if name then -- if attachment slot is not empty .. save it table.insert(addon.Attachments, { ["icon"] = icon, ["link"] = link, ["count"] = count } ) end end end local function SendGuildMail(recipient, subject, body) local player = DataStore:GetNameOfMain(recipient) if not player then return end -- this mail is sent to "player", but is for alt "recipient" GuildWhisper(player, MSG_SENDMAIL_INIT, recipient) if type(addon.Attachments) == "table" then for _, attachment in pairs(addon.Attachments) do GuildWhisper(player, MSG_SENDMAIL_ATTACHMENT, attachment.icon, attachment.link, attachment.count) end end -- .. then save the mail itself + gold if any local money = GetSendMailMoney() if (money > 0) or (strlen(body) > 0) then GuildWhisper(player, MSG_SENDMAIL_BODY, subject, body, money) end GuildWhisper(player, MSG_SENDMAIL_END) end -- *** Scanning functions *** local function SaveAttachments(character, index, mailSender, days, wasReturned) -- saves attachments of a given mail index into a given character mailbox for attachmentIndex = 1, 12 do -- mandatory, loop through all 12 slots, since attachments could be anywhere (ex: slot 4,5,8) local item, mailIcon, itemCount = GetInboxItem(index, attachmentIndex) if item then table.insert(character.Mails, { icon = mailIcon, count = itemCount, link = GetInboxItemLink(index, attachmentIndex), sender = mailSender, lastCheck = time(), daysLeft = days, returned = wasReturned, } ) end end end local function ScanMailbox() local character = addon.ThisCharacter wipe(character.Mails) local numItems = GetInboxNumItems(); if numItems == 0 then return end local cache = character.MailCache -- check the cache, and clean entries that are about to be replaced in the scan for i = #cache, 1, -1 do if cache[i].lastCheck then local age = time() - cache[i].lastCheck if age > 3600 then -- if older than 1 hour, delete this entry table.remove(cache, i) end end end for i = 1, numItems do local _, stationaryIcon, mailSender, mailSubject, mailMoney, _, days, numAttachments, _, wasReturned = GetInboxHeaderInfo(i); if numAttachments then -- treat attachments as separate entries SaveAttachments(character, i, mailSender, days, wasReturned) end local inboxText if GetOption("ScanMailBody") == 1 then inboxText = GetInboxText(i) -- this marks the mail as read end if (mailMoney > 0) or inboxText then -- if there's money or text .. save the entry local mailIcon if mailMoney > 0 then mailIcon = ICON_COIN else mailIcon = stationaryIcon end table.insert(character.Mails, { icon = mailIcon, money = mailMoney, text = inboxText, subject = mailSubject, sender = mailSender, lastCheck = time(), daysLeft = days, returned = wasReturned, } ) end end -- show mails with the lowest expiry first table.sort(character.Mails, function(a, b) return a.daysLeft < b.daysLeft end) end -- *** Event Handlers *** -- local function OnBagUpdate(event, bag) local function OnBagUpdate(arg) local bag = arg FCSP_timer_OnBagUpdate = nil -- manual reset (safety redundancy) if addon.isOpen then -- if a bag is updated while the mailbox is opened, this means an attachment has been taken. -- print("DataStore_Mails.lua -- OnBagUpdate(event, ",bag,")", DEBUG_CNT, format("%.3f",GetTime()%100)) -- DEBUG 2025 07 21 - 2 ScanMailbox() -- I could not hook TakeInboxItem because mailbox content is not updated yet end end local function FCSP_OnBagUpdate(event, bag) -- this function limits calls to "OnBagUpdate" to max 1 every second if bag < 0 then return end if addon.isOpen then -- if a bag is updated while the mailbox is opened, this means an attachment has been taken. if FCSP_timer_OnBagUpdate then return end -- FCSP_timer_OnBagUpdate = addon:ScheduleTimer(OnBagUpdate, 1, event, bag) FCSP_timer_OnBagUpdate = addon:ScheduleTimer(OnBagUpdate, 1, bag) end end local function OnMailInboxUpdate() -- process only one occurence of the event, right after MAIL_SHOW addon:UnregisterEvent("MAIL_INBOX_UPDATE") ScanMailbox() end local function OnMailSendInfoUpdate() ReadMailAttachments() end local function OnMailClosed() addon.isOpen = nil addon:UnregisterEvent("MAIL_CLOSED") ScanMailbox() local character = addon.ThisCharacter character.lastUpdate = time() character.lastVisitDate = date("%Y/%m/%d %H:%M") addon:UnregisterEvent("MAIL_SEND_INFO_UPDATE") wipe(addon.Attachments) end local function OnMailShow() -- the event may be triggered multiple times, exit if the mailbox is already open if addon.isOpen then return end CheckInbox() addon:RegisterEvent("MAIL_CLOSED", OnMailClosed) addon:RegisterEvent("MAIL_INBOX_UPDATE", OnMailInboxUpdate) addon:RegisterEvent("MAIL_SEND_INFO_UPDATE", OnMailSendInfoUpdate) -- create a temporary table to hold the attachments that will be sent, keep it local since the event is rare addon.Attachments = addon.Attachments or {} addon.isOpen = true end -- ** Mixins ** local function _GetMailboxLastVisit(character) return character.lastUpdate or 0 end local function _GetMailItemCount(character, searchedID) local count = 0 for _, v in pairs (character.Mails) do local link = v.link if link and (GetIDFromLink(link) == searchedID) then count = count + (v.count or 1) end end for _, v in pairs (character.MailCache) do local link = v.link if link and (GetIDFromLink(link) == searchedID) then count = count + (v.count or 1) end end return count end local function _GetMailAttachments() return addon.Attachments end local function _GetNumMails(character) return #character.Mails + #character.MailCache end local function _GetMailInfo(character, index) local data = GetMailTable(character, index) return data.icon, data.count, data.link, data.money, data.text, data.returned end local function _GetMailSender(character, index) local data = GetMailTable(character, index) return data.sender end local function _GetMailExpiry(character, index) local data = GetMailTable(character, index) -- return the mail expiry, expressed in days and in seconds local diff = time() - data.lastCheck local days = data.daysLeft - (diff / 86400) local seconds = (data.daysLeft * 86400) - diff return days, seconds end local function _GetMailSubject(character, index) local data = GetMailTable(character, index) if data.subject then -- if there's a subject, use it return data.subject end -- otherwise, return the name of the item local name local link = data.link if link then local id = GetIDFromLink(link) name = GetItemInfo(id) end return name end local function _GetNumExpiredMails(character, threshold) local count = 0 for i = 1, _GetNumMails(character) do if _GetMailExpiry(character, i) < threshold then count = count + 1 end end return count end local function _SaveMailToCache(character, mailMoney, mailBody, mailSubject, mailSender) local mailIcon = (mailMoney > 0) and ICON_COIN or ICON_NOTE table.insert(character.MailCache, { money = mailMoney, icon = mailIcon, text = mailBody, subject = mailSubject, sender = mailSender, lastCheck = time(), daysLeft = MAIL_EXPIRY, } ) end local function _SaveMailAttachmentToCache(character, mailIcon, mailLink, mailCount, mailSender) table.insert(character.MailCache, { icon = mailIcon, link = mailLink, count = mailCount, sender = mailSender, lastCheck = time(), daysLeft = MAIL_EXPIRY, } ) end local function _IsMailBoxOpen() return addon.isOpen end local PublicMethods = { GetMailboxLastVisit = _GetMailboxLastVisit, GetMailItemCount = _GetMailItemCount, GetMailAttachments = _GetMailAttachments, GetNumMails = _GetNumMails, GetMailInfo = _GetMailInfo, GetMailSender = _GetMailSender, GetMailExpiry = _GetMailExpiry, GetMailSubject = _GetMailSubject, GetNumExpiredMails = _GetNumExpiredMails, SaveMailToCache = _SaveMailToCache, SaveMailAttachmentToCache = _SaveMailAttachmentToCache, IsMailBoxOpen = _IsMailBoxOpen, } -- *** Guild Comm *** local guildMailRecipient -- name of the alt who receives a mail from a guildmate local guildMailRecipientKey -- key of the alt who receives a mail from a guildmate local GuildCommCallbacks = { [MSG_SENDMAIL_INIT] = function(sender, recipient) guildMailRecipient = recipient guildMailRecipientKey = format("%s.%s.%s", THIS_ACCOUNT, GetRealmName(), recipient) end, [MSG_SENDMAIL_END] = function(sender) if guildMailRecipient then addon:SendMessage("DATASTORE_GUILD_MAIL_RECEIVED", sender, guildMailRecipient) end guildMailRecipient = nil guildMailRecipientKey = nil end, [MSG_SENDMAIL_ATTACHMENT] = function(sender, icon, link, count) local recipientTable = addon.db.global.Characters[guildMailRecipientKey] if recipientTable then _SaveMailAttachmentToCache(recipientTable, icon, link, count, sender) end end, [MSG_SENDMAIL_BODY] = function(sender, subject, body, money) local recipientTable = addon.db.global.Characters[guildMailRecipientKey] if recipientTable then _SaveMailToCache(recipientTable, money, body, subject, sender) end end, } local function CheckExpiries() local allAccounts = GetOption("CheckMailExpiryAllAccounts") local allRealms = GetOption("CheckMailExpiryAllRealms") local threshold = GetOption("MailWarningThreshold") local expiryFound local account, realm for key, character in pairs(addon.db.global.Characters) do account, realm = strsplit(".", key) if allAccounts == 1 or ((allAccounts == 0) and (account == THIS_ACCOUNT)) then -- all accounts, or only current and current was found if allRealms == 1 or ((allRealms == 0) and (realm == GetRealmName())) then -- all realms, or only current and current was found -- detect return vs delete local numExpiredMails = _GetNumExpiredMails(character, threshold) if numExpiredMails > 0 then expiryFound = true addon:SendMessage("DATASTORE_MAIL_EXPIRY", character, key, threshold, numExpiredMails) end end end end if expiryFound then -- global expiry message, register this one if your addon just wants to know that at least one mail has expired, and you don't care which. addon:SendMessage("DATASTORE_GLOBAL_MAIL_EXPIRY", threshold) end end function addon:OnInitialize() addon.db = LibStub("AceDB-3.0"):New(addonName .. "DB", AddonDB_Defaults) DataStore:RegisterModule(addonName, addon, PublicMethods) DataStore:SetGuildCommCallbacks(commPrefix, GuildCommCallbacks) DataStore:SetCharacterBasedMethod("GetMailboxLastVisit") DataStore:SetCharacterBasedMethod("GetMailItemCount") DataStore:SetCharacterBasedMethod("GetNumMails") DataStore:SetCharacterBasedMethod("GetMailInfo") DataStore:SetCharacterBasedMethod("GetMailSender") DataStore:SetCharacterBasedMethod("GetMailExpiry") DataStore:SetCharacterBasedMethod("GetMailSubject") DataStore:SetCharacterBasedMethod("GetNumExpiredMails") DataStore:SetCharacterBasedMethod("SaveMailToCache") DataStore:SetCharacterBasedMethod("SaveMailAttachmentToCache") addon:RegisterComm(commPrefix, DataStore:GetGuildCommHandler()) end function addon:OnEnable() addon:RegisterEvent("MAIL_SHOW", OnMailShow) -- addon:RegisterEvent("BAG_UPDATE", OnBagUpdate) addon:RegisterEvent("BAG_UPDATE", FCSP_OnBagUpdate) addon:SetupOptions() if GetOption("CheckMailExpiry") == 1 then addon:ScheduleTimer(CheckExpiries, 5) -- check mail expiries 5 seconds later, to decrease the load at startup end end function addon:OnDisable() addon:UnregisterEvent("MAIL_SHOW") addon:UnregisterEvent("BAG_UPDATE") end -- *** Hooks *** local Orig_SendMail = SendMail function SendMail(recipient, subject, body, ...) -- this function takes care of saving mails sent to alts directly into their mailbox, so that client addons don't have to take care about it local isRecipientAnAlt for characterName, characterKey in pairs(DataStore:GetCharacters()) do -- browse alts on current realm if strlower(characterName) == strlower(recipient) then -- if recipient is a known alt .. local character = addon.db.global.Characters[characterKey] for k, v in pairs(addon.Attachments) do -- .. save attachments into his mailbox table.insert(character.Mails, { -- not in the mail cache, since they arrive directly in an alt's mailbox icon = v.icon, link = v.link, count = v.count, sender = UnitName("player"), lastCheck = time(), daysLeft = MAIL_EXPIRY, } ) end -- .. then save the mail itself + gold if any local moneySent = GetSendMailMoney() if (moneySent > 0) or (strlen(body) > 0) then local mailIcon if moneySent > 0 then mailIcon = ICON_COIN else mailIcon = ICON_NOTE end table.insert(character.Mails, { money = moneySent, icon = mailIcon, text = body, subject = subject, sender = UnitName("player"), lastCheck = time(), daysLeft = MAIL_EXPIRY, } ) end -- if the alt has never checked his mail before, this value won't be correct, so set it to make sure expiry returns proper results. character.lastUpdate = time() table.sort(character.Mails, function(a, b) -- show mails with the lowest expiry first return a.daysLeft < b.daysLeft end) isRecipientAnAlt = true break end end if not isRecipientAnAlt then -- if recipient is not an alt, maybe it's a guildmate SendGuildMail(recipient, subject, body) end Orig_SendMail(recipient, subject, body, ...) end local Orig_ReturnInboxItem = ReturnInboxItem function ReturnInboxItem(index, ...) local _, stationaryIcon, mailSender, mailSubject, mailMoney, _, _, numAttachments = GetInboxHeaderInfo(index) local isRecipientAnAlt local inboxText = "" for characterName, characterKey in pairs(DataStore:GetCharacters()) do -- browse alts on current realm if strlower(characterName) == strlower(mailSender) then -- if recipient is a known alt .. local character = addon.db.global.Characters[characterKey] if numAttachments then -- treat attachments as separate entries SaveAttachments(character, index, UnitName("player"), MAIL_EXPIRY, true) end inboxText = GetInboxText(index) -- this marks the mail as read, no problem here since the mail is returned anyway if (mailMoney > 0) or inboxText then -- if there's money or text .. save the entry local mailIcon if mailMoney > 0 then mailIcon = ICON_COIN else mailIcon = stationaryIcon end table.insert(character.Mails, { icon = mailIcon, money = mailMoney, text = inboxText, subject = mailSubject, sender = UnitName("player"), lastCheck = time(), daysLeft = MAIL_EXPIRY, returned = true, -- this is the mail we're returning, so true } ) end isRecipientAnAlt = true break end end if not isRecipientAnAlt then -- if recipient is not an alt, maybe it's a guildmate ReadMailAttachments(index) SendGuildMail(mailSender, mailSubject, inboxText) end Orig_ReturnInboxItem(index, ...) end