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