1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 #include "nsMsgPurgeService.h"
7 #include "nsIMsgAccountManager.h"
8 #include "nsMsgBaseCID.h"
9 #include "nsMsgUtils.h"
10 #include "nsMsgSearchCore.h"
11 #include "msgCore.h"
12 #include "nsISpamSettings.h"
13 #include "nsIMsgSearchTerm.h"
14 #include "nsIMsgHdr.h"
15 #include "nsIMsgProtocolInfo.h"
16 #include "nsIMsgFilterPlugin.h"
17 #include "nsIPrefBranch.h"
18 #include "nsIPrefService.h"
19 #include "mozilla/Logging.h"
20 #include "nsMsgFolderFlags.h"
21 #include <stdlib.h>
22 #include "nsComponentManagerUtils.h"
23 #include "nsServiceManagerUtils.h"
24 
25 static mozilla::LazyLogModule MsgPurgeLogModule("MsgPurge");
26 
NS_IMPL_ISUPPORTS(nsMsgPurgeService,nsIMsgPurgeService,nsIMsgSearchNotify)27 NS_IMPL_ISUPPORTS(nsMsgPurgeService, nsIMsgPurgeService, nsIMsgSearchNotify)
28 
29 void OnPurgeTimer(nsITimer* timer, void* aPurgeService) {
30   nsMsgPurgeService* purgeService = (nsMsgPurgeService*)aPurgeService;
31   purgeService->PerformPurge();
32 }
33 
nsMsgPurgeService()34 nsMsgPurgeService::nsMsgPurgeService() {
35   mHaveShutdown = false;
36   // never purge a folder more than once every 8 hours (60 min/hour * 8 hours.
37   mMinDelayBetweenPurges = 480;
38   // fire the purge timer every 5 minutes, starting 5 minutes after the service
39   // is created (when we load accounts).
40   mPurgeTimerInterval = 5;
41 }
42 
~nsMsgPurgeService()43 nsMsgPurgeService::~nsMsgPurgeService() {
44   if (mPurgeTimer) mPurgeTimer->Cancel();
45 
46   if (!mHaveShutdown) Shutdown();
47 }
48 
Init()49 NS_IMETHODIMP nsMsgPurgeService::Init() {
50   nsresult rv;
51 
52   // these prefs are here to help QA test this feature
53   nsCOMPtr<nsIPrefBranch> prefBranch(
54       do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
55   if (NS_SUCCEEDED(rv)) {
56     int32_t min_delay;
57     rv = prefBranch->GetIntPref("mail.purge.min_delay", &min_delay);
58     if (NS_SUCCEEDED(rv) && min_delay) mMinDelayBetweenPurges = min_delay;
59 
60     int32_t purge_timer_interval;
61     rv = prefBranch->GetIntPref("mail.purge.timer_interval",
62                                 &purge_timer_interval);
63     if (NS_SUCCEEDED(rv) && purge_timer_interval)
64       mPurgeTimerInterval = purge_timer_interval;
65   }
66 
67   MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
68           ("mail.purge.min_delay=%d minutes", mMinDelayBetweenPurges));
69   MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
70           ("mail.purge.timer_interval=%d minutes", mPurgeTimerInterval));
71 
72   // don't start purging right away.
73   // because the accounts aren't loaded and because the user might be trying to
74   // sign in or startup, etc.
75   SetupNextPurge();
76 
77   mHaveShutdown = false;
78   return NS_OK;
79 }
80 
Shutdown()81 NS_IMETHODIMP nsMsgPurgeService::Shutdown() {
82   if (mPurgeTimer) {
83     mPurgeTimer->Cancel();
84     mPurgeTimer = nullptr;
85   }
86 
87   mHaveShutdown = true;
88   return NS_OK;
89 }
90 
SetupNextPurge()91 nsresult nsMsgPurgeService::SetupNextPurge() {
92   MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
93           ("setting to check again in %d minutes", mPurgeTimerInterval));
94 
95   // Convert mPurgeTimerInterval into milliseconds
96   uint32_t timeInMSUint32 = mPurgeTimerInterval * 60000;
97 
98   // Can't currently reset a timer when it's in the process of
99   // calling Notify. So, just release the timer here and create a new one.
100   if (mPurgeTimer) mPurgeTimer->Cancel();
101 
102   mPurgeTimer = do_CreateInstance("@mozilla.org/timer;1");
103   mPurgeTimer->InitWithNamedFuncCallback(
104       OnPurgeTimer, (void*)this, timeInMSUint32, nsITimer::TYPE_ONE_SHOT,
105       "nsMsgPurgeService::OnPurgeTimer");
106 
107   return NS_OK;
108 }
109 
110 // This is the function that looks for the first folder to purge. It also
111 // applies retention settings to any folder that hasn't had retention settings
112 // applied in mMinDelayBetweenPurges minutes (default, 8 hours).
113 // However, if we've spent more than .5 seconds in this loop, don't
114 // apply any more retention settings because it might lock up the UI.
115 // This might starve folders later on in the hierarchy, since we always
116 // start at the top, but since we also apply retention settings when you
117 // open a folder, or when you compact all folders, I think this will do
118 // for now, until we have a cleanup on shutdown architecture.
PerformPurge()119 nsresult nsMsgPurgeService::PerformPurge() {
120   MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("performing purge"));
121 
122   nsresult rv;
123 
124   nsCOMPtr<nsIMsgAccountManager> accountManager =
125       do_GetService(NS_MSGACCOUNTMANAGER_CONTRACTID, &rv);
126   NS_ENSURE_SUCCESS(rv, rv);
127   bool keepApplyingRetentionSettings = true;
128 
129   nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
130   rv = accountManager->GetAllServers(allServers);
131   if (NS_SUCCEEDED(rv)) {
132     MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
133             ("%d servers", (int)allServers.Length()));
134     nsCOMPtr<nsIMsgFolder> folderToPurge;
135     PRIntervalTime startTime = PR_IntervalNow();
136     int32_t purgeIntervalToUse = 0;
137     PRTime oldestPurgeTime =
138         0;  // we're going to pick the least-recently purged folder
139 
140     // apply retention settings to folders that haven't had retention settings
141     // applied in mMinDelayBetweenPurges minutes (default 8 hours)
142     // Because we get last purge time from the folder cache,
143     // this code won't open db's for folders until it decides it needs
144     // to apply retention settings, and since
145     // nsIMsgFolder::ApplyRetentionSettings will close any db's it opens, this
146     // code won't leave db's open.
147     for (uint32_t serverIndex = 0; serverIndex < allServers.Length();
148          serverIndex++) {
149       nsCOMPtr<nsIMsgIncomingServer> server(allServers[serverIndex]);
150       if (server) {
151         if (keepApplyingRetentionSettings) {
152           nsCOMPtr<nsIMsgFolder> rootFolder;
153           rv = server->GetRootFolder(getter_AddRefs(rootFolder));
154           NS_ENSURE_SUCCESS(rv, rv);
155 
156           nsTArray<RefPtr<nsIMsgFolder>> childFolders;
157           rv = rootFolder->GetDescendants(childFolders);
158           NS_ENSURE_SUCCESS(rv, rv);
159 
160           for (auto childFolder : childFolders) {
161             uint32_t folderFlags;
162             (void)childFolder->GetFlags(&folderFlags);
163             if (folderFlags & nsMsgFolderFlags::Virtual) continue;
164             PRTime curFolderLastPurgeTime = 0;
165             nsCString curFolderLastPurgeTimeString, curFolderUri;
166             rv = childFolder->GetStringProperty("LastPurgeTime",
167                                                 curFolderLastPurgeTimeString);
168             if (NS_FAILED(rv))
169               continue;  // it is ok to fail, go on to next folder
170 
171             if (!curFolderLastPurgeTimeString.IsEmpty()) {
172               PRTime theTime;
173               PR_ParseTimeString(curFolderLastPurgeTimeString.get(), false,
174                                  &theTime);
175               curFolderLastPurgeTime = theTime;
176             }
177 
178             childFolder->GetURI(curFolderUri);
179             MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
180                     ("%s curFolderLastPurgeTime=%s (if blank, then never)",
181                      curFolderUri.get(), curFolderLastPurgeTimeString.get()));
182 
183             // check if this folder is due to purge
184             // has to have been purged at least mMinDelayBetweenPurges minutes
185             // ago we don't want to purge the folders all the time - once a
186             // day is good enough
187             int64_t minDelayBetweenPurges(mMinDelayBetweenPurges);
188             int64_t microSecondsPerMinute(60000000);
189             PRTime nextPurgeTime =
190                 curFolderLastPurgeTime +
191                 (minDelayBetweenPurges * microSecondsPerMinute);
192             if (nextPurgeTime < PR_Now()) {
193               MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
194                       ("purging %s", curFolderUri.get()));
195               childFolder->ApplyRetentionSettings();
196             }
197             PRIntervalTime elapsedTime = PR_IntervalNow() - startTime;
198             // check if more than 500 milliseconds have elapsed in this purge
199             // process
200             if (PR_IntervalToMilliseconds(elapsedTime) > 500) {
201               keepApplyingRetentionSettings = false;
202               break;
203             }
204           }
205         }
206         nsCString type;
207         nsresult rv = server->GetType(type);
208         NS_ENSURE_SUCCESS(rv, rv);
209 
210         nsCString realHostName;
211         server->GetRealHostName(realHostName);
212         MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
213                 ("[%d] %s (%s)", serverIndex, realHostName.get(), type.get()));
214 
215         nsCOMPtr<nsISpamSettings> spamSettings;
216         rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
217         NS_ENSURE_SUCCESS(rv, rv);
218 
219         int32_t spamLevel;
220         spamSettings->GetLevel(&spamLevel);
221         // clang-format off
222         MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
223                 ("[%d] spamLevel=%d (if 0, don't purge)", serverIndex, spamLevel));
224         // clang-format on
225         if (!spamLevel) continue;
226 
227         // check if we are set up to purge for this server
228         // if not, skip it.
229         bool purgeSpam;
230         spamSettings->GetPurge(&purgeSpam);
231 
232         MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
233                 ("[%d] purgeSpam=%s (if false, don't purge)", serverIndex,
234                  purgeSpam ? "true" : "false"));
235         if (!purgeSpam) continue;
236 
237         // check if the spam folder uri is set for this server
238         // if not skip it.
239         nsCString junkFolderURI;
240         rv = spamSettings->GetSpamFolderURI(junkFolderURI);
241         NS_ENSURE_SUCCESS(rv, rv);
242 
243         MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
244                 ("[%d] junkFolderURI=%s (if empty, don't purge)", serverIndex,
245                  junkFolderURI.get()));
246         if (junkFolderURI.IsEmpty()) continue;
247 
248         // if the junk folder doesn't exist
249         // because the folder pane isn't built yet, for example
250         // skip this account
251         nsCOMPtr<nsIMsgFolder> junkFolder;
252         rv = FindFolder(junkFolderURI, getter_AddRefs(junkFolder));
253         NS_ENSURE_SUCCESS(rv, rv);
254 
255         // clang-format off
256         MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
257                 ("[%d] %s exists? %s (if doesn't exist, don't purge)", serverIndex,
258                  junkFolderURI.get(), junkFolder ? "true" : "false"));
259         // clang-format on
260         if (!junkFolder) continue;
261 
262         PRTime curJunkFolderLastPurgeTime = 0;
263         nsCString curJunkFolderLastPurgeTimeString;
264         rv = junkFolder->GetStringProperty("curJunkFolderLastPurgeTime",
265                                            curJunkFolderLastPurgeTimeString);
266         if (NS_FAILED(rv))
267           continue;  // it is ok to fail, junk folder may not exist
268 
269         if (!curJunkFolderLastPurgeTimeString.IsEmpty()) {
270           PRTime theTime;
271           PR_ParseTimeString(curJunkFolderLastPurgeTimeString.get(), false,
272                              &theTime);
273           curJunkFolderLastPurgeTime = theTime;
274         }
275 
276         MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
277                 ("[%d] %s curJunkFolderLastPurgeTime=%s (if blank, then never)",
278                  serverIndex, junkFolderURI.get(),
279                  curJunkFolderLastPurgeTimeString.get()));
280 
281         // check if this account is due to purge
282         // has to have been purged at least mMinDelayBetweenPurges minutes ago
283         // we don't want to purge the folders all the time
284         PRTime nextPurgeTime =
285             curJunkFolderLastPurgeTime +
286             mMinDelayBetweenPurges * 60000000  // convert to microseconds.
287             ;
288         if (nextPurgeTime < PR_Now()) {
289           MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
290                   ("[%d] last purge greater than min delay", serverIndex));
291 
292           nsCOMPtr<nsIMsgIncomingServer> junkFolderServer;
293           rv = junkFolder->GetServer(getter_AddRefs(junkFolderServer));
294           NS_ENSURE_SUCCESS(rv, rv);
295 
296           bool serverBusy = false;
297           bool serverRequiresPassword = true;
298           bool passwordPromptRequired;
299           bool canSearchMessages = false;
300           junkFolderServer->GetPasswordPromptRequired(&passwordPromptRequired);
301           junkFolderServer->GetServerBusy(&serverBusy);
302           junkFolderServer->GetServerRequiresPasswordForBiff(
303               &serverRequiresPassword);
304           junkFolderServer->GetCanSearchMessages(&canSearchMessages);
305           // Make sure we're logged on before doing the search (assuming we need
306           // to be) and make sure the server isn't already in the middle of
307           // downloading new messages and make sure a search isn't already going
308           // on
309           MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
310                   ("[%d] (search in progress? %s)", serverIndex,
311                    mSearchSession ? "true" : "false"));
312           MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
313                   ("[%d] (server busy? %s)", serverIndex,
314                    serverBusy ? "true" : "false"));
315           MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
316                   ("[%d] (serverRequiresPassword? %s)", serverIndex,
317                    serverRequiresPassword ? "true" : "false"));
318           MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
319                   ("[%d] (passwordPromptRequired? %s)", serverIndex,
320                    passwordPromptRequired ? "true" : "false"));
321           if (canSearchMessages && !mSearchSession && !serverBusy &&
322               (!serverRequiresPassword || !passwordPromptRequired)) {
323             int32_t purgeInterval;
324             spamSettings->GetPurgeInterval(&purgeInterval);
325 
326             if ((oldestPurgeTime == 0) ||
327                 (curJunkFolderLastPurgeTime < oldestPurgeTime)) {
328               // clang-format off
329               MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
330                       ("[%d] purging! searching for messages older than %d days",
331                        serverIndex, purgeInterval));
332               // clang-format on
333               oldestPurgeTime = curJunkFolderLastPurgeTime;
334               purgeIntervalToUse = purgeInterval;
335               folderToPurge = junkFolder;
336               // if we've never purged this folder, do it...
337               if (curJunkFolderLastPurgeTime == 0) break;
338             }
339           } else {
340             NS_ASSERTION(canSearchMessages,
341                          "unexpected, you should be able to search");
342             MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
343                     ("[%d] not a good time for this server, try again later",
344                      serverIndex));
345           }
346         } else {
347           MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
348                   ("[%d] last purge too recent", serverIndex));
349         }
350       }
351     }
352     if (folderToPurge && purgeIntervalToUse != 0)
353       rv = SearchFolderToPurge(folderToPurge, purgeIntervalToUse);
354   }
355 
356   // set up timer to check accounts again
357   SetupNextPurge();
358   return rv;
359 }
360 
SearchFolderToPurge(nsIMsgFolder * folder,int32_t purgeInterval)361 nsresult nsMsgPurgeService::SearchFolderToPurge(nsIMsgFolder* folder,
362                                                 int32_t purgeInterval) {
363   nsresult rv;
364   mSearchSession = do_CreateInstance(NS_MSGSEARCHSESSION_CONTRACTID, &rv);
365   NS_ENSURE_SUCCESS(rv, rv);
366   mSearchSession->RegisterListener(this, nsIMsgSearchSession::allNotifications);
367 
368   // update the time we attempted to purge this folder
369   char dateBuf[100];
370   dateBuf[0] = '\0';
371   PRExplodedTime exploded;
372   PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
373   PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%a %b %d %H:%M:%S %Y",
374                          &exploded);
375   folder->SetStringProperty("curJunkFolderLastPurgeTime",
376                             nsDependentCString(dateBuf));
377   MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
378           ("curJunkFolderLastPurgeTime is now %s", dateBuf));
379 
380   nsCOMPtr<nsIMsgIncomingServer> server;
381   // We need to get the folder's server scope because imap can have
382   // local junk folder.
383   rv = folder->GetServer(getter_AddRefs(server));
384   NS_ENSURE_SUCCESS(rv, rv);
385 
386   nsMsgSearchScopeValue searchScope;
387   server->GetSearchScope(&searchScope);
388 
389   mSearchSession->AddScopeTerm(searchScope, folder);
390 
391   // look for messages older than the cutoff
392   // you can't also search by junk status, see
393   // nsMsgPurgeService::OnSearchHit()
394   nsCOMPtr<nsIMsgSearchTerm> searchTerm;
395   mSearchSession->CreateTerm(getter_AddRefs(searchTerm));
396   if (searchTerm) {
397     searchTerm->SetAttrib(nsMsgSearchAttrib::AgeInDays);
398     searchTerm->SetOp(nsMsgSearchOp::IsGreaterThan);
399     nsCOMPtr<nsIMsgSearchValue> searchValue;
400     searchTerm->GetValue(getter_AddRefs(searchValue));
401     if (searchValue) {
402       searchValue->SetAttrib(nsMsgSearchAttrib::AgeInDays);
403       searchValue->SetAge((uint32_t)purgeInterval);
404       searchTerm->SetValue(searchValue);
405     }
406     searchTerm->SetBooleanAnd(false);
407     mSearchSession->AppendTerm(searchTerm);
408   }
409 
410   // we are about to search
411   // create mHdrsToDelete array (if not previously created)
412   NS_ASSERTION(mHdrsToDelete.IsEmpty(), "mHdrsToDelete is not empty");
413 
414   mSearchFolder = folder;
415   return mSearchSession->Search(nullptr);
416 }
417 
OnNewSearch()418 NS_IMETHODIMP nsMsgPurgeService::OnNewSearch() {
419   MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("on new search"));
420   return NS_OK;
421 }
422 
OnSearchHit(nsIMsgDBHdr * aMsgHdr,nsIMsgFolder * aFolder)423 NS_IMETHODIMP nsMsgPurgeService::OnSearchHit(nsIMsgDBHdr* aMsgHdr,
424                                              nsIMsgFolder* aFolder) {
425   NS_ENSURE_ARG_POINTER(aMsgHdr);
426 
427   nsCString messageId;
428   nsCString author;
429   nsCString subject;
430 
431   aMsgHdr->GetMessageId(getter_Copies(messageId));
432   MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
433           ("messageId=%s", messageId.get()));
434   aMsgHdr->GetSubject(getter_Copies(subject));
435   MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
436           ("subject=%s", subject.get()));
437   aMsgHdr->GetAuthor(getter_Copies(author));
438   MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
439           ("author=%s", author.get()));
440 
441   // double check that the message is junk before adding to
442   // the list of messages to delete
443   //
444   // note, we can't just search for messages that are junk
445   // because not all imap server support keywords
446   // (which we use for the junk score)
447   // so the junk status would be in the message db.
448   //
449   // see bug #194090
450   nsCString junkScoreStr;
451   nsresult rv =
452       aMsgHdr->GetStringProperty("junkscore", getter_Copies(junkScoreStr));
453   NS_ENSURE_SUCCESS(rv, rv);
454 
455   MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
456           ("junkScore=%s (if empty or != nsIJunkMailPlugin::IS_SPAM_SCORE, "
457            "don't add to list delete)",
458            junkScoreStr.get()));
459 
460   // if "junkscore" is not set, don't delete the message
461   if (junkScoreStr.IsEmpty()) return NS_OK;
462 
463   if (atoi(junkScoreStr.get()) == nsIJunkMailPlugin::IS_SPAM_SCORE) {
464     MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
465             ("added message to delete"));
466     mHdrsToDelete.AppendElement(aMsgHdr);
467   }
468   return NS_OK;
469 }
470 
OnSearchDone(nsresult status)471 NS_IMETHODIMP nsMsgPurgeService::OnSearchDone(nsresult status) {
472   if (NS_SUCCEEDED(status)) {
473     uint32_t count = mHdrsToDelete.Length();
474     MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
475             ("%d messages to delete", count));
476 
477     if (count > 0) {
478       MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("delete messages"));
479       if (mSearchFolder)
480         mSearchFolder->DeleteMessages(
481             mHdrsToDelete, nullptr, false /*delete storage*/, false /*isMove*/,
482             nullptr, false /*allowUndo*/);
483     }
484   }
485   mHdrsToDelete.Clear();
486   if (mSearchSession) mSearchSession->UnregisterListener(this);
487   // don't cache the session
488   // just create another search session next time we search, rather than
489   // clearing scopes, terms etc. we also use mSearchSession to determine if the
490   // purge service is "busy"
491   mSearchSession = nullptr;
492   mSearchFolder = nullptr;
493   return NS_OK;
494 }
495