/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- * * The contents of this file are subject to the Netscape Public * License Version 1.1 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.mozilla.org/NPL/ * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is Netscape * Communications Corporation. Portions created by Netscape are * Copyright (C) 1998 Netscape Communications Corporation. All * Rights Reserved. * * Contributor(s): */ #include "msg.h" #include "xp.h" #include "newsdb.h" #include "dberror.h" #include "newshdr.h" #include "msgdbvw.h" #include "msghdr.h" #include "thrhead.h" #include "grpinfo.h" #include "msgmast.h" #include "newshost.h" #include "msgfinfo.h" #include "newsset.h" #include "xpgetstr.h" #include "pmsgfilt.h" #include "jsmsg.h" #include "msgdbapi.h" extern "C" { extern int MK_MSG_EXPIRE_COUNT; extern int MK_MSG_DONE_EXPIRE_COUNT; } NewsGroupDB::NewsGroupDB() { m_groupURL = NULL; m_filterList = NULL; m_headerIndex = 0; m_master = NULL; } NewsGroupDB::~NewsGroupDB() { if (m_filterList != NULL) MSG_CancelFilterList(m_filterList); FREEIF(m_groupURL); JSFilter_cleanup(); } // inline virtuals put in .cpp file to help some compilers int NewsGroupDB::GetCurVersion() {return kNewsDBVersion;} NewsGroupDB *NewsGroupDB::GetNewsDB() {return this;} // groupURL is just the url constructed by msg_MakeNewsURL, e.g., // "alt.fan.h-h-hippeaux//newshost:20/". MsgERR NewsGroupDB::MessageDBOpenUsingURL(const char * groupURL) { MsgERR err; char *groupName; m_groupURL = XP_STRDUP(groupURL); char *hostName = NET_ParseURL (groupURL, GET_HOST_PART); MSG_NewsHost* host = m_master->FindHost(hostName, *groupURL == 's', -1); FREEIF(hostName); if (!host) return eUNKNOWN; // ###tw groupName = GetGroupNameFromURL(groupURL); m_info = host->FindGroup(groupName); FREEIF(groupName); if (!m_info) return eUNKNOWN; // ###tw // OK, break java script news filters if the user has defined filters for this group. MSG_FilterError filterErr = MSG_FilterList::Open(m_master, filterNews, NULL, m_info, &m_filterList); int32 filterCount = 0; if (filterErr != FilterError_Success || (m_filterList->GetFilterCount(&filterCount) == FilterError_Success && filterCount == 0)) { if (m_filterList) MSG_CancelFilterList(m_filterList); if (MSG_FilterList::Open(m_master, filterInbox, &m_filterList) != FilterError_Success) m_filterList = NULL; } // get root category if we have a category container... m_info = m_info->GetNewsFolderInfo(); m_set = m_info->GetSet(); const char* dbFileName = m_info->GetDBFileName(); err = MessageDB::MessageDBOpen(dbFileName, TRUE); // XP_FREE(dbFileName); return err; } MsgERR NewsGroupDB::Open(const char * groupURL, MSG_Master *master, NewsGroupDB** pMessageDB) { NewsGroupDB *newsDB; XP_ASSERT(master != NULL); if (master == NULL) return eUNKNOWN; char *hostName = NET_ParseURL (groupURL, GET_HOST_PART); MSG_NewsHost* host = master->FindHost(hostName, *groupURL == 's', -1); if (!host) { // ###tw This should probably be allowed. If someone types a news: // URL by hand, then we should go create the host and stuff here. // Not right now, though. return eUNKNOWN; } FREEIF(hostName); if (m_cacheEnabled) { char* groupname = GetGroupNameFromURL(groupURL); if (!groupname) return eUNKNOWN; // Bad URL? MSG_FolderInfoNews* info = host->FindGroup(groupname); FREEIF(groupname); if (!info) return eUNKNOWN; // New newsgroup? Probably should add... // ###tw const char *groupFile = info->GetDBFileName(); XP_ASSERT(groupFile); if (!groupFile) return eUNKNOWN; newsDB = (NewsGroupDB *) FindInCache(groupFile); if (newsDB) { *pMessageDB = newsDB; // make this behave like the non-cache case. ++newsDB->m_useCount; return(eSUCCESS); } } newsDB = new NewsGroupDB; if (!newsDB) { *pMessageDB = NULL; return(eOUT_OF_MEMORY); } newsDB->m_master = master; MsgERR err = newsDB->MessageDBOpenUsingURL(groupURL); if (err == eSUCCESS) { *pMessageDB = newsDB; if (m_cacheEnabled) GetDBCache()->Add(newsDB); newsDB->HandleLatered(); if (newsDB->m_dbFolderInfo != NULL) newsDB->GetNewsFolderInfo()->SetNewsDB(newsDB); } else { *pMessageDB = NULL; delete newsDB; } return(err); } MsgERR NewsGroupDB::Commit(XP_Bool compress /* = FALSE */) { return MessageDB::Commit(compress); } // caller needs to RemoveReference when finished. NewsMessageHdr *NewsGroupDB::GetNewsHdrForKey(MessageKey messageKey) { NewsMessageHdr *headerObject = NULL; MSG_HeaderHandle headerHandle = MSG_DBHandle_GetHandleForKey(m_dbHandle, messageKey); if (headerHandle) { headerObject = new NewsMessageHdr; headerObject->SetHandle(headerHandle); } return headerObject; } // Expire up to but not including expireKey. MsgERR NewsGroupDB::ExpireUpTo(MessageKey expireKey, MWContext *context) { MsgERR err; XP_ASSERT(m_dbFolderInfo != NULL); if (m_dbFolderInfo == NULL) return eSUCCESS; // pass in 0 in case expiredMark is wrong - should'nt cost much since there // probably aren't any articles with id's below expiredMark. if ((err = ExpireRange(0 /*m_dbFolderInfo->m_expiredMark*/, --expireKey, context)) == eSUCCESS) m_dbFolderInfo->SetExpiredMark(expireKey); if (err == eCorruptDB) SetSummaryValid(FALSE); return err; } MsgERR NewsGroupDB::ExpireRange(MessageKey startRange, MessageKey endRange, MWContext *context) { MsgERR ret = MSG_DBHandle_ExpireRange(m_dbHandle, startRange, endRange); m_set->AddRange(startRange, endRange); char msgBuf[100]; const char * msgTemplate = XP_GetString(MK_MSG_EXPIRE_COUNT); //DMB TODO - what about removing these from the view, and giving progress? // if (removeCount % 10 == 0) // { // PR_snprintf (msgBuf, sizeof(msgBuf), msgTemplate, removeCount); // FE_Progress (context, msgBuf); // } // } // msgTemplate = XP_GetString(MK_MSG_DONE_EXPIRE_COUNT); // PR_snprintf (msgBuf, sizeof(msgBuf), msgTemplate, removeCount); // FE_Progress (context, msgBuf); return eSUCCESS; } // return highest article number we've seen. Returns 0 in case of errors. MessageKey NewsGroupDB::GetHighwaterArticleNum() { XP_ASSERT(m_dbFolderInfo != NULL); if (m_dbFolderInfo == NULL) return 0; return m_dbFolderInfo->GetHighWater(); } // return the key of the first article number we know about. // Since the iterator iterates in id order, we can just grab the // messagekey of the first header it returns. // ### dmb // This will not deal with the situation where we get holes in // the headers we know about. Need to figure out how and when // to solve that. This could happen if a transfer is interrupted. // Do we need to keep track of known arts permanently? MessageKey NewsGroupDB::GetLowWaterArticleNum() { ListContext *pContext; DBMessageHdr *pFirstHdr; MessageKey lowWater = 0; if (ListFirst(&pContext, &pFirstHdr) == eSUCCESS) { lowWater = pFirstHdr->GetMessageKey(); delete pFirstHdr; ListDone(pContext); } return lowWater; } // This function merely parses the XOver header into a DBMessageHdr // and stores that in m_headers. From there, it will be threaded // and added to the database in a separate pass. MsgERR NewsGroupDB::AddHdrFromXOver(const char * line, MessageKey *msgId) { MsgERR err = eSUCCESS; char copyHeader[1024]; DBMessageHdr *msgHdr = new NewsMessageHdr; XP_STRNCPY_SAFE(copyHeader, line, sizeof(copyHeader)); if (!msgHdr->InitFromXOverLine(copyHeader, m_dbHandle)) { // XP_TRACE("Failed to parse XOver line %s\n", line); return eXOverParseError; } if (msgId) *msgId = msgHdr->GetMessageKey(); m_newHeaders.SetAtGrow(m_headerIndex++, msgHdr); if (m_headerIndex > 200) { for (int i = 0; i < m_newHeaders.GetSize(); i++) { XP_Bool isNewThread; DBMessageHdr *dbMsgHdr = (DBMessageHdr *) m_newHeaders[i]; err = AddHdrToDB(dbMsgHdr, &isNewThread); delete dbMsgHdr; // ### dmb - should we really ignore the rest of the headers? if (err != eSUCCESS) break; } m_newHeaders.RemoveAll(); m_headerIndex = 0; } return err; } MsgERR NewsGroupDB::AddHdrToDB(DBMessageHdr *newHdr, XP_Bool *newThread, XP_Bool notify /* = FALSE */) { MsgERR err = eSUCCESS; XP_Bool bRead; XPStringObj msgId; XP_Bool addToDB = TRUE; MessageKey highWaterMark = newHdr->m_messageKey; IsRead(newHdr->m_messageKey, &bRead); if (bRead) newHdr->OrFlags(kIsRead); else newHdr->OrFlags(kNew); #if 0 // DMB TODO - we could have an api that checks if duplicate header exists DBMessageHdr *dbHeader = GetDBMessageHdrForHashMessageID(newHdr->m_messageIdID); { if (dbHeader != NULL) { if (dbHeader->GetLineCount() == newHdr->GetLineCount()) { XPStringObj newSubject, dbHdrSubject; if (newHdr->GetSubject(newSubject, TRUE, GetDB()) && dbHeader->GetSubject(dbHdrSubject, TRUE, GetDB())) { addToDB = XP_STRCMP(newSubject, dbHdrSubject); if (!addToDB) { MarkHdrRead(newHdr, TRUE, NULL); #ifdef DEBUG_bienvenu XP_Trace("got duplicate message-id marking read and ignoring\n"); #endif delete newHdr; // forget about this header } } } delete dbHeader; } } #endif // kill files should be here. if (addToDB) { if (!bRead) { int32 filterCount = 0; MSG_Filter *filter; if (m_filterList != NULL) m_filterList->GetFilterCount(&filterCount); for (MSG_FilterIndex filterIndex = 0; filterIndex < filterCount; filterIndex++) { if (m_filterList->GetFilterAt(filterIndex, &filter) == FilterError_Success) { if (!filter->GetEnabled()) continue; if (filter->GetType() == filterNewsJavaScript) { if (JSNewsFilter_execute(filter, newHdr, this) == FilterError_Success) break; } else if (filter->GetType() == filterNewsRule) { MSG_Rule *rule; MSG_SearchError matchTermStatus; if (filter->GetRule(&rule) == FilterError_Success) { { // put this in its own block so scope will get destroyed // before we apply the actions, so folder file will get closed. MSG_ScopeTerm scope (NULL, scopeOfflineNewsgroup, m_info); matchTermStatus = msg_SearchOfflineMail::MatchTermsForSearch(newHdr, rule->GetTermList(), &scope, this); } if (matchTermStatus == SearchError_Success) { MSG_RuleActionType actionType; void *value = NULL; if (rule->GetAction(&actionType, &value) == FilterError_Success) { switch (actionType) { case acDelete: addToDB = FALSE; break; case acMarkRead: MarkHdrRead(newHdr, TRUE, NULL); break; case acKillThread: // for ignore and watch, we will need the db // to check for the flags in msgHdr's that // get added, because only then will we know // the thread they're getting added to. newHdr->OrFlags(kIgnored); break; case acWatchThread: newHdr->OrFlags(kWatched); break; case acChangePriority: SetPriority(newHdr, * ((MSG_PRIORITY *) &value)); break; default: break; } } break; } } } } } } } if (addToDB) err = MessageDB::AddHdrToDB(newHdr, newThread, notify); // what if we just followed a link to an article number??? ### dmb TODO m_dbFolderInfo->SetHighWater(highWaterMark); return err; } // force the database to close - this'll flush out anybody holding onto // a database without having a listener! MsgERR NewsGroupDB::ForceClosed() { m_info = NULL; // if we're forcing closed at shutdown, m_info might be already deleted. return MessageDB::ForceClosed(); } MsgERR NewsGroupDB::Close() { // if we're really closing, make sure any changes to the newsrc file get // saved. if (m_useCount <= 1 && m_info) { m_info->GetHost()->WriteIfDirty(); } MessageDB::Close(); return eSUCCESS; } DBFolderInfo *NewsGroupDB::CreateFolderInfo(MSG_DBFolderInfoHandle handle) { return new NewsFolderInfo((handle) ? handle : MSG_CreateNewsDBFolderInfo()); } MsgERR NewsGroupDB::MarkHdrRead(DBMessageHdr *msgHdr, XP_Bool bRead, ChangeListener *instigator) { MessageKey messageKey = msgHdr->GetMessageKey(); int err; if (bRead) err = m_set->Add(messageKey); else err = m_set->Remove(messageKey); // give parent class chance to update data structures MessageDB::MarkHdrRead(msgHdr, bRead, instigator); return (err >= 0) ? eSUCCESS : eOUT_OF_MEMORY; } MsgERR NewsGroupDB::IsRead(MessageKey messageKey, XP_Bool *pRead) { XP_ASSERT(pRead != NULL); if (pRead == NULL) return eUNKNOWN; XP_Bool isRead = m_set->IsMember(messageKey); *pRead = isRead; return eSUCCESS; } MsgERR NewsGroupDB::MarkAllRead(MWContext *context, IDArray * /*thoseMarked*/) { if (GetLowWaterArticleNum() > 2) m_set->AddRange(1, GetLowWaterArticleNum() - 1); MsgERR err = MessageDB::MarkAllRead(context); if (err == eSUCCESS && 1 <= GetHighwaterArticleNum()) m_set->AddRange(1, GetHighwaterArticleNum()); // mark everything read in newsrc. return err; } // caller needs to free return char * char *NewsGroupDB::GetGroupNameFromURL(const char *url) { char *groupPart; char *groupName; groupPart = groupName = NET_ParseURL (url, GET_PATH_PART); if (!groupPart) return NULL; // ### dmb (tw) - needs someone who knows how to parse newsgroup urls if (groupName[0] == '/') { groupName = XP_STRDUP(groupPart + 1); FREEIF(groupPart); } return groupName; } MsgERR NewsGroupDB::ListNextUnread(ListContext **pContext, DBMessageHdr **pResult) { MsgERR dbErr = eSUCCESS; XP_Bool lastWasRead = TRUE; MessageKey nextKey = 0; *pResult = NULL; while (TRUE) { if (*pContext == NULL) { *pContext = new ListContext; if (!*pContext) return eOUT_OF_MEMORY; (*pContext)->m_pMessageDB = this; (*pContext)->m_curMessageNum = nextKey = GetLowWaterArticleNum(); } else { // this works if we are marking things read as we go. // We really need a NextNonMember function which takes // the place to start. nextKey = MAX(m_set->FirstNonMember(), nextKey + 1); } if (dbErr == eDBEndOfList || nextKey > GetHighwaterArticleNum()) { delete *pContext; break; } // this currently doesn't happen since ListNext doesn't return errors // other than eDBEndOfList. else if (dbErr != eSUCCESS) break; if (IsRead(nextKey, &lastWasRead) == eSUCCESS && !lastWasRead) { DBMessageHdr *msgHdr = GetNewsHdrForKey(nextKey); if (msgHdr) { *pResult = msgHdr; (*pContext)->m_curMessageNum = nextKey; break; } else { m_set->Add(nextKey); // mark read in newsrc file, so FirstNonMember will work } } else if (lastWasRead) // first non-member seems horked - try adding this key to set m_set->Add(nextKey); } return dbErr; } XP_Bool NewsGroupDB::IsArticleOffline(MessageKey messageKey) { XP_Bool ret = FALSE; NewsMessageHdr *newsHdr = GetNewsHdrForKey(messageKey); if (newsHdr != NULL) { ret = (newsHdr->GetFlags() & kOffline); delete newsHdr; } return ret; } XP_Bool NewsGroupDB::IsCategory() { return m_info && m_info->TestFlag(MSG_FOLDER_FLAG_CATEGORY); } XP_Bool NewsGroupDB::PurgeNeeded(MSG_PurgeInfo *purgeHdrPref, MSG_PurgeInfo *purgeArtPref) { MSG_PurgeInfo curHdrPurgeInfo; MSG_PurgeInfo curBodyPurgeInfo; GetPurgeHeaderInfo(&curHdrPurgeInfo); GetPurgeArticleInfo(&curBodyPurgeInfo); if (curHdrPurgeInfo.m_useDefaults) curHdrPurgeInfo = *purgeHdrPref; if (curBodyPurgeInfo.m_useDefaults) curBodyPurgeInfo = *purgeArtPref; if (curHdrPurgeInfo.m_purgeBy == MSG_PurgeNone && curBodyPurgeInfo.m_purgeBy == MSG_PurgeNone) return FALSE; if (curHdrPurgeInfo.m_purgeBy == MSG_PurgeByNumHeaders && m_dbFolderInfo->GetNumMessages() > curHdrPurgeInfo.m_numHeadersToKeep) return TRUE; long daysToKeep = 0; if (curHdrPurgeInfo.m_purgeBy == MSG_PurgeByAge) daysToKeep = curHdrPurgeInfo.m_daysToKeep; if (curBodyPurgeInfo.m_purgeBy == MSG_PurgeByAge) daysToKeep = (daysToKeep == 0) ? curBodyPurgeInfo.m_daysToKeep : MIN(daysToKeep, curBodyPurgeInfo.m_daysToKeep); if (curHdrPurgeInfo.m_purgeBy == MSG_PurgeByAge || curBodyPurgeInfo.m_purgeBy == MSG_PurgeByAge) { // need to add a use default setting for per newsgroup purging preferences. ListContext *listContext = NULL; while (TRUE) { MsgERR dbErr; DBMessageHdr *pHeader = NULL; if (listContext == NULL) dbErr = ListFirst (&listContext, &pHeader); else dbErr = ListNext(listContext, &pHeader); if (dbErr != eSUCCESS) dbErr = eDBEndOfList; if (dbErr != eSUCCESS /* eDBEndOfList */) { dbErr = eSUCCESS; ListDone(listContext); break; } time_t now = XP_TIME(); long matchDay = now - (daysToKeep * 60 * 60 * 24); long msgDate = pHeader->GetDate(); delete pHeader; if (msgDate < matchDay) { ListDone(listContext); return TRUE; } } } return FALSE; } MsgERR NewsGroupDB::SetOfflineRetrievalInfo(MSG_RetrieveArtInfo *info) { XP_ASSERT(m_dbFolderInfo != NULL); if (m_dbFolderInfo == NULL) return eDBNotOpen; GetNewsFolderInfo()->SetOfflineRetrievalInfo(info); return eSUCCESS; } MsgERR NewsGroupDB::SetPurgeHeaderInfo(MSG_PurgeInfo *purgeInfo) { XP_ASSERT(m_dbFolderInfo != NULL); if (m_dbFolderInfo == NULL) return eDBNotOpen; GetNewsFolderInfo()->SetPurgeHeaderInfo(purgeInfo); return eSUCCESS; } MsgERR NewsGroupDB::SetPurgeArticleInfo(MSG_PurgeInfo *purgeInfo) { XP_ASSERT(m_dbFolderInfo != NULL); if (m_dbFolderInfo == NULL) return eDBNotOpen; GetNewsFolderInfo()->SetPurgeArticleInfo(purgeInfo); return eSUCCESS; } MsgERR NewsGroupDB::GetOfflineRetrievalInfo(MSG_RetrieveArtInfo *info) { XP_ASSERT(m_dbFolderInfo != NULL); if (m_dbFolderInfo == NULL) return eDBNotOpen; GetNewsFolderInfo()->GetOfflineRetrievalInfo(info); return eSUCCESS; } MsgERR NewsGroupDB::GetPurgeHeaderInfo(MSG_PurgeInfo *purgeInfo) { XP_ASSERT(m_dbFolderInfo != NULL); if (m_dbFolderInfo == NULL) return eDBNotOpen; return GetNewsFolderInfo()->GetPurgeHeaderInfo(purgeInfo); } MsgERR NewsGroupDB::GetPurgeArticleInfo(MSG_PurgeInfo *purgeInfo) { XP_ASSERT(m_dbFolderInfo != NULL); if (m_dbFolderInfo == NULL) return eDBNotOpen; return GetNewsFolderInfo()->GetPurgeArticleInfo(purgeInfo); }