-- Guda Mailbox Scanner -- Scans and stores mailbox contents local addon = Guda local MailboxScanner = {} addon.Modules.MailboxScanner = MailboxScanner local mailboxOpen = false local savePending = false local SAVE_DEBOUNCE = 0.5 -- Debounced save: coalesces bursts of MAIL_INBOX_UPDATE into a single rescan. -- Scanning 100+ mails on every event was causing client crashes. local function ScheduleSave() if savePending then return end if not mailboxOpen then return end savePending = true Guda_ScheduleTimer(SAVE_DEBOUNCE, function() savePending = false if mailboxOpen then MailboxScanner:SaveToDatabase() end end) end -- Scan all mailbox items and return data function MailboxScanner:ScanMailbox() if not mailboxOpen then addon:Debug("Cannot scan mailbox - not open") return {} end local mailboxData = {} local numItems = GetInboxNumItems() for i = 1, numItems do local mailRows = self:ScanMailItemRows(i) for _, row in ipairs(mailRows) do table.insert(mailboxData, row) end end return mailboxData end -- Scan a single mail into one or more rows (flattened) function MailboxScanner:ScanMailItemRows(index) -- GetInboxHeaderInfo(index) returns: packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM local packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM = GetInboxHeaderInfo(index) local rows = {} if hasItem then -- Turtle WoW supports up to 12 attachments per mail. -- We use GetInboxNumAttachments if available to avoid over-scanning on servers -- that might ignore the second argument of GetInboxItem. local numAttachments = 0 if GetInboxNumAttachments then numAttachments = GetInboxNumAttachments(index) or 0 end -- Fallback: if we don't have the count but header says there's an item, assume at least 1. if numAttachments == 0 and hasItem then numAttachments = 1 end for itemIndex = 1, numAttachments do -- GetInboxItem(index, itemIndex) returns: name, texture, count, quality, canUse local name, texture, count, quality, canUse = GetInboxItem(index, itemIndex) if not name then break end local itemLink = addon.Modules.Utils:GetInboxItemLink(index, itemIndex) local itemID = itemLink and addon.Modules.Utils:ExtractItemID(itemLink) -- Fallback 1: If link/itemID is missing, try GetItemInfo(name) which might be cached now if not itemID or not itemLink then local _, link = GetItemInfo(name) if link then itemLink = link itemID = addon.Modules.Utils:ExtractItemID(link) addon:Debug("Recovered link from GetItemInfo for %s", name) end end -- (Removed) FindItemByName cross-character fallback: with cross-account -- data this scales as mails * attachments * characters * slots and could -- crash the client on large mailboxes. GetItemInfo above handles the -- common case; missing links resolve on the next rescan once cached. local itemData = { link = itemLink, texture = texture or "Interface\\Icons\\INV_Misc_Bag_08", count = count or 1, quality = quality or 0, name = name, itemID = itemID, } -- Ensure we have a link if we have an itemID if itemData.itemID and not itemData.link then itemData.link = "item:" .. itemData.itemID .. ":0:0:0" end -- If we have a link, try to get more detailed info from cache if itemData.link then local itemName, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemData.link) if itemName then itemData.name = itemName itemData.quality = itemQuality or itemData.quality itemData.iLevel = iLevel itemData.type = itemType itemData.class = itemCategory itemData.subclass = itemSubType itemData.equipSlot = itemEquipLoc if itemTexture then itemData.texture = itemTexture end end end table.insert(rows, { sender = sender, subject = subject, money = (itemIndex == 1) and money or 0, -- Attach money only to the first row of this mail CODAmount = (itemIndex == 1) and CODAmount or 0, daysLeft = daysLeft, hasItem = true, item = itemData, mailIndex = index, itemIndex = itemIndex, wasRead = wasRead, packageIcon = packageIcon, }) end end -- If no items found but there is money or it's just a letter if table.getn(rows) == 0 then table.insert(rows, { sender = sender, subject = subject, money = money, CODAmount = CODAmount, daysLeft = daysLeft, hasItem = false, item = nil, mailIndex = index, itemIndex = 1, wasRead = wasRead, packageIcon = packageIcon, }) end return rows end -- Save current mailbox to database function MailboxScanner:SaveToDatabase() if not mailboxOpen then return end local mailboxData = self:ScanMailbox() addon.Modules.DB:SaveMailbox(mailboxData) addon:Debug("Mailbox data saved") end -- Build item data from name/texture/count/quality local function BuildSendMailItemData(name, texture, count, quality) local _, link = GetItemInfo(name) local itemData = { name = name, texture = texture or "Interface\\Icons\\INV_Misc_Bag_08", count = count or 1, quality = quality or 0, link = link, itemID = addon.Modules.Utils:ExtractItemID(link), } local itemName, retLink, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc = addon.Modules.Utils:GetItemInfo(name) if itemName then itemData.link = retLink or itemData.link itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link) or itemData.itemID itemData.quality = itemQuality or itemData.quality itemData.iLevel = iLevel itemData.type = itemType itemData.class = itemCategory itemData.subclass = itemSubType itemData.equipSlot = itemEquipLoc if itemTexture then itemData.texture = itemTexture end end if not itemData.itemID and itemData.link then itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link) end return itemData end -- Handle outgoing mail function MailboxScanner:OnSendMail(recipient, subject, body) if not recipient or recipient == "" then return end local moneyAmount = GetSendMailMoney() local subjectText = (subject and subject ~= "") and subject or "No Subject" local senderName = UnitName("player") local addedAny = false -- Try multi-attachment (TurtleWoW): check if HasSendMailItem exists if HasSendMailItem then for itemIndex = 1, 12 do if not HasSendMailItem(itemIndex) then break end local name, texture, count, quality = GetSendMailItem(itemIndex) if not name then break end local mailRow = { sender = senderName, subject = subjectText, money = (itemIndex == 1) and moneyAmount or 0, CODAmount = 0, daysLeft = 30, hasItem = true, item = BuildSendMailItemData(name, texture, count, quality), wasRead = false, } addon.Modules.DB:AddMailToCharacter(recipient, nil, mailRow) addedAny = true end end -- Fallback: single attachment (vanilla API) if not addedAny then local name, texture, count, quality = GetSendMailItem() if name then local mailRow = { sender = senderName, subject = subjectText, money = moneyAmount, CODAmount = 0, daysLeft = 30, hasItem = true, item = BuildSendMailItemData(name, texture, count, quality), wasRead = false, } addon.Modules.DB:AddMailToCharacter(recipient, nil, mailRow) addedAny = true end end -- Money-only mail (no items) if not addedAny and moneyAmount > 0 then local mailRow = { sender = senderName, subject = subjectText, money = moneyAmount, CODAmount = 0, daysLeft = 30, hasItem = false, item = nil, wasRead = false, } addon.Modules.DB:AddMailToCharacter(recipient, nil, mailRow) end end -- Handle auction house buyouts function MailboxScanner:OnAuctionBid(type, index, bid) local name, texture, count, quality, canUse, level, minBid, minIncrement, buyoutPrice, bidAmount, highBidder, owner = GetAuctionItemInfo(type, index) if name and buyoutPrice > 0 and bid >= buyoutPrice then local link = GetAuctionItemLink(type, index) local itemData = { name = name, texture = texture or "Interface\\Icons\\INV_Misc_Bag_08", count = count or 1, quality = quality or 0, link = link, itemID = addon.Modules.Utils:ExtractItemID(link), } -- Try to get more info if it's in cache local itemName, retLink, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(link or name) if itemName then itemData.link = retLink or itemData.link itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link) or itemData.itemID itemData.quality = itemQuality or itemData.quality itemData.iLevel = iLevel itemData.type = itemType itemData.class = itemCategory itemData.subclass = itemSubType itemData.equipSlot = itemEquipLoc if itemTexture then itemData.texture = itemTexture end end local mailRow = { sender = "Auction House", subject = "Auction won: " .. name, money = 0, CODAmount = 0, daysLeft = 30, hasItem = true, item = itemData, wasRead = false, } addon.Modules.DB:AddMailToCharacter(UnitName("player"), nil, mailRow) addon:Debug("Captured AH buyout: %s", name) end end -- Initialize mailbox scanner function MailboxScanner:Initialize() -- Hook SendMail to capture outgoing mail to alts local originalSendMail = SendMail SendMail = function(recipient, subject, body) MailboxScanner:OnSendMail(recipient, subject, body) return originalSendMail(recipient, subject, body) end -- Hook PlaceAuctionBid to capture AH buyouts local originalPlaceAuctionBid = PlaceAuctionBid PlaceAuctionBid = function(type, index, bid) MailboxScanner:OnAuctionBid(type, index, bid) return originalPlaceAuctionBid(type, index, bid) end -- Hook taking items/money (original calls only, DB save handled by MAIL_INBOX_UPDATE) local originalTakeInboxItem = TakeInboxItem TakeInboxItem = function(index, attachmentIndex) return originalTakeInboxItem(index, attachmentIndex) end local originalTakeInboxMoney = TakeInboxMoney TakeInboxMoney = function(index) return originalTakeInboxMoney(index) end local originalAutoLootMailItem = AutoLootMailItem AutoLootMailItem = function(index) return originalAutoLootMailItem(index) end -- Rescan mailbox when server confirms inbox changes (debounced) addon.Modules.Events:Register("MAIL_INBOX_UPDATE", function() ScheduleSave() end, "MailboxScanner_InboxUpdate") -- Mailbox opened addon.Modules.Events:OnMailShow(function() mailboxOpen = true addon:Debug("Mailbox opened") ScheduleSave() end, "MailboxScanner") -- Mailbox closed addon.Modules.Events:OnMailClosed(function() -- Final save on close (synchronous so it can't race with mailboxOpen=false) if mailboxOpen then MailboxScanner:SaveToDatabase() end mailboxOpen = false savePending = false addon:Debug("Mailbox closed") end, "MailboxScanner") end -- Check if mailbox is currently open function MailboxScanner:IsMailboxOpen() return mailboxOpen end