summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/src/nsMsgPurgeService.cpp
blob: 0f72416c7ba553c5cc3f743b48db01f12f9e4444 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsMsgPurgeService.h"
#include "nsIMsgAccountManager.h"
#include "nsMsgUtils.h"
#include "nsMsgSearchCore.h"
#include "msgCore.h"
#include "nsISpamSettings.h"
#include "nsIMsgSearchTerm.h"
#include "nsIMsgHdr.h"
#include "nsIMsgProtocolInfo.h"
#include "nsIMsgFilterPlugin.h"
#include "nsIPrefBranch.h"
#include "nsIPrefService.h"
#include "mozilla/Logging.h"
#include "nsMsgFolderFlags.h"
#include "nsITimer.h"
#include <stdlib.h>
#include "nsComponentManagerUtils.h"
#include "nsServiceManagerUtils.h"

static mozilla::LazyLogModule MsgPurgeLogModule("MsgPurge");

NS_IMPL_ISUPPORTS(nsMsgPurgeService, nsIMsgPurgeService, nsIMsgSearchNotify)

void OnPurgeTimer(nsITimer* timer, void* aPurgeService) {
  nsMsgPurgeService* purgeService = (nsMsgPurgeService*)aPurgeService;
  purgeService->PerformPurge();
}

nsMsgPurgeService::nsMsgPurgeService() {
  mHaveShutdown = false;
  // never purge a folder more than once every 8 hours (60 min/hour * 8 hours.
  mMinDelayBetweenPurges = 480;
  // fire the purge timer every 5 minutes, starting 5 minutes after the service
  // is created (when we load accounts).
  mPurgeTimerInterval = 5;
}

nsMsgPurgeService::~nsMsgPurgeService() {
  if (mPurgeTimer) mPurgeTimer->Cancel();

  if (!mHaveShutdown) Shutdown();
}

NS_IMETHODIMP nsMsgPurgeService::Init() {
  nsresult rv;

  // these prefs are here to help QA test this feature
  nsCOMPtr<nsIPrefBranch> prefBranch(
      do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
  if (NS_SUCCEEDED(rv)) {
    int32_t min_delay;
    rv = prefBranch->GetIntPref("mail.purge.min_delay", &min_delay);
    if (NS_SUCCEEDED(rv) && min_delay) mMinDelayBetweenPurges = min_delay;

    int32_t purge_timer_interval;
    rv = prefBranch->GetIntPref("mail.purge.timer_interval",
                                &purge_timer_interval);
    if (NS_SUCCEEDED(rv) && purge_timer_interval)
      mPurgeTimerInterval = purge_timer_interval;
  }

  MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
          ("mail.purge.min_delay=%d minutes", mMinDelayBetweenPurges));
  MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
          ("mail.purge.timer_interval=%d minutes", mPurgeTimerInterval));

  // don't start purging right away.
  // because the accounts aren't loaded and because the user might be trying to
  // sign in or startup, etc.
  SetupNextPurge();

  mHaveShutdown = false;
  return NS_OK;
}

NS_IMETHODIMP nsMsgPurgeService::Shutdown() {
  if (mPurgeTimer) {
    mPurgeTimer->Cancel();
    mPurgeTimer = nullptr;
  }

  mHaveShutdown = true;
  return NS_OK;
}

nsresult nsMsgPurgeService::SetupNextPurge() {
  MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
          ("setting to check again in %d minutes", mPurgeTimerInterval));

  // Convert mPurgeTimerInterval into milliseconds
  uint32_t timeInMSUint32 = mPurgeTimerInterval * 60000;

  // Can't currently reset a timer when it's in the process of
  // calling Notify. So, just release the timer here and create a new one.
  if (mPurgeTimer) mPurgeTimer->Cancel();

  nsresult rv = NS_NewTimerWithFuncCallback(
      getter_AddRefs(mPurgeTimer), OnPurgeTimer, (void*)this, timeInMSUint32,
      nsITimer::TYPE_ONE_SHOT, "nsMsgPurgeService::OnPurgeTimer", nullptr);
  if (NS_FAILED(rv)) {
    NS_WARNING("Could not start mPurgeTimer timer");
  }

  return NS_OK;
}

// This is the function that looks for the first folder to purge. It also
// applies retention settings to any folder that hasn't had retention settings
// applied in mMinDelayBetweenPurges minutes (default, 8 hours).
// However, if we've spent more than .5 seconds in this loop, don't
// apply any more retention settings because it might lock up the UI.
// This might starve folders later on in the hierarchy, since we always
// start at the top, but since we also apply retention settings when you
// open a folder, or when you compact all folders, I think this will do
// for now, until we have a cleanup on shutdown architecture.
nsresult nsMsgPurgeService::PerformPurge() {
  MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("performing purge"));

  nsresult rv;

  nsCOMPtr<nsIMsgAccountManager> accountManager =
      do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  bool keepApplyingRetentionSettings = true;

  nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
  rv = accountManager->GetAllServers(allServers);
  if (NS_SUCCEEDED(rv)) {
    MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
            ("%d servers", (int)allServers.Length()));
    nsCOMPtr<nsIMsgFolder> folderToPurge;
    PRIntervalTime startTime = PR_IntervalNow();
    int32_t purgeIntervalToUse = 0;
    PRTime oldestPurgeTime =
        0;  // we're going to pick the least-recently purged folder

    // apply retention settings to folders that haven't had retention settings
    // applied in mMinDelayBetweenPurges minutes (default 8 hours)
    // Because we get last purge time from the folder cache,
    // this code won't open db's for folders until it decides it needs
    // to apply retention settings, and since
    // nsIMsgFolder::ApplyRetentionSettings will close any db's it opens, this
    // code won't leave db's open.
    for (uint32_t serverIndex = 0; serverIndex < allServers.Length();
         serverIndex++) {
      nsCOMPtr<nsIMsgIncomingServer> server(allServers[serverIndex]);
      if (server) {
        if (keepApplyingRetentionSettings) {
          nsCOMPtr<nsIMsgFolder> rootFolder;
          rv = server->GetRootFolder(getter_AddRefs(rootFolder));
          NS_ENSURE_SUCCESS(rv, rv);

          nsTArray<RefPtr<nsIMsgFolder>> childFolders;
          rv = rootFolder->GetDescendants(childFolders);
          NS_ENSURE_SUCCESS(rv, rv);

          for (auto childFolder : childFolders) {
            uint32_t folderFlags;
            (void)childFolder->GetFlags(&folderFlags);
            if (folderFlags & nsMsgFolderFlags::Virtual) continue;
            PRTime curFolderLastPurgeTime = 0;
            nsCString curFolderLastPurgeTimeString, curFolderUri;
            rv = childFolder->GetStringProperty("LastPurgeTime",
                                                curFolderLastPurgeTimeString);
            if (NS_FAILED(rv))
              continue;  // it is ok to fail, go on to next folder

            if (!curFolderLastPurgeTimeString.IsEmpty()) {
              PRTime theTime;
              PR_ParseTimeString(curFolderLastPurgeTimeString.get(), false,
                                 &theTime);
              curFolderLastPurgeTime = theTime;
            }

            childFolder->GetURI(curFolderUri);
            MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                    ("%s curFolderLastPurgeTime=%s (if blank, then never)",
                     curFolderUri.get(), curFolderLastPurgeTimeString.get()));

            // check if this folder is due to purge
            // has to have been purged at least mMinDelayBetweenPurges minutes
            // ago we don't want to purge the folders all the time - once a
            // day is good enough
            int64_t minDelayBetweenPurges(mMinDelayBetweenPurges);
            int64_t microSecondsPerMinute(60000000);
            PRTime nextPurgeTime =
                curFolderLastPurgeTime +
                (minDelayBetweenPurges * microSecondsPerMinute);
            if (nextPurgeTime < PR_Now()) {
              MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                      ("purging %s", curFolderUri.get()));
              childFolder->ApplyRetentionSettings();
            }
            PRIntervalTime elapsedTime = PR_IntervalNow() - startTime;
            // check if more than 500 milliseconds have elapsed in this purge
            // process
            if (PR_IntervalToMilliseconds(elapsedTime) > 500) {
              keepApplyingRetentionSettings = false;
              break;
            }
          }
        }
        nsCString type;
        nsresult rv = server->GetType(type);
        NS_ENSURE_SUCCESS(rv, rv);

        nsCString hostName;
        server->GetHostName(hostName);
        MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                ("[%d] %s (%s)", serverIndex, hostName.get(), type.get()));

        nsCOMPtr<nsISpamSettings> spamSettings;
        rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
        NS_ENSURE_SUCCESS(rv, rv);

        int32_t spamLevel;
        spamSettings->GetLevel(&spamLevel);
        // clang-format off
        MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                ("[%d] spamLevel=%d (if 0, don't purge)", serverIndex, spamLevel));
        // clang-format on
        if (!spamLevel) continue;

        // check if we are set up to purge for this server
        // if not, skip it.
        bool purgeSpam;
        spamSettings->GetPurge(&purgeSpam);

        MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                ("[%d] purgeSpam=%s (if false, don't purge)", serverIndex,
                 purgeSpam ? "true" : "false"));
        if (!purgeSpam) continue;

        // check if the spam folder uri is set for this server
        // if not skip it.
        nsCString junkFolderURI;
        rv = spamSettings->GetSpamFolderURI(junkFolderURI);
        NS_ENSURE_SUCCESS(rv, rv);

        MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                ("[%d] junkFolderURI=%s (if empty, don't purge)", serverIndex,
                 junkFolderURI.get()));
        if (junkFolderURI.IsEmpty()) continue;

        // if the junk folder doesn't exist
        // because the folder pane isn't built yet, for example
        // skip this account
        nsCOMPtr<nsIMsgFolder> junkFolder;
        rv = FindFolder(junkFolderURI, getter_AddRefs(junkFolder));
        NS_ENSURE_SUCCESS(rv, rv);

        // clang-format off
        MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                ("[%d] %s exists? %s (if doesn't exist, don't purge)", serverIndex,
                 junkFolderURI.get(), junkFolder ? "true" : "false"));
        // clang-format on
        if (!junkFolder) continue;

        PRTime curJunkFolderLastPurgeTime = 0;
        nsCString curJunkFolderLastPurgeTimeString;
        rv = junkFolder->GetStringProperty("curJunkFolderLastPurgeTime",
                                           curJunkFolderLastPurgeTimeString);
        if (NS_FAILED(rv))
          continue;  // it is ok to fail, junk folder may not exist

        if (!curJunkFolderLastPurgeTimeString.IsEmpty()) {
          PRTime theTime;
          PR_ParseTimeString(curJunkFolderLastPurgeTimeString.get(), false,
                             &theTime);
          curJunkFolderLastPurgeTime = theTime;
        }

        MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                ("[%d] %s curJunkFolderLastPurgeTime=%s (if blank, then never)",
                 serverIndex, junkFolderURI.get(),
                 curJunkFolderLastPurgeTimeString.get()));

        // check if this account is due to purge
        // has to have been purged at least mMinDelayBetweenPurges minutes ago
        // we don't want to purge the folders all the time
        PRTime nextPurgeTime =
            curJunkFolderLastPurgeTime +
            mMinDelayBetweenPurges * 60000000  // convert to microseconds.
            ;
        if (nextPurgeTime < PR_Now()) {
          MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                  ("[%d] last purge greater than min delay", serverIndex));

          nsCOMPtr<nsIMsgIncomingServer> junkFolderServer;
          rv = junkFolder->GetServer(getter_AddRefs(junkFolderServer));
          NS_ENSURE_SUCCESS(rv, rv);

          bool serverBusy = false;
          bool serverRequiresPassword = true;
          bool passwordPromptRequired;
          bool canSearchMessages = false;
          junkFolderServer->GetPasswordPromptRequired(&passwordPromptRequired);
          junkFolderServer->GetServerBusy(&serverBusy);
          junkFolderServer->GetServerRequiresPasswordForBiff(
              &serverRequiresPassword);
          junkFolderServer->GetCanSearchMessages(&canSearchMessages);
          // Make sure we're logged on before doing the search (assuming we need
          // to be) and make sure the server isn't already in the middle of
          // downloading new messages and make sure a search isn't already going
          // on
          MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                  ("[%d] (search in progress? %s)", serverIndex,
                   mSearchSession ? "true" : "false"));
          MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                  ("[%d] (server busy? %s)", serverIndex,
                   serverBusy ? "true" : "false"));
          MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                  ("[%d] (serverRequiresPassword? %s)", serverIndex,
                   serverRequiresPassword ? "true" : "false"));
          MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                  ("[%d] (passwordPromptRequired? %s)", serverIndex,
                   passwordPromptRequired ? "true" : "false"));
          if (canSearchMessages && !mSearchSession && !serverBusy &&
              (!serverRequiresPassword || !passwordPromptRequired)) {
            int32_t purgeInterval;
            spamSettings->GetPurgeInterval(&purgeInterval);

            if ((oldestPurgeTime == 0) ||
                (curJunkFolderLastPurgeTime < oldestPurgeTime)) {
              // clang-format off
              MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                      ("[%d] purging! searching for messages older than %d days",
                       serverIndex, purgeInterval));
              // clang-format on
              oldestPurgeTime = curJunkFolderLastPurgeTime;
              purgeIntervalToUse = purgeInterval;
              folderToPurge = junkFolder;
              // if we've never purged this folder, do it...
              if (curJunkFolderLastPurgeTime == 0) break;
            }
          } else {
            NS_ASSERTION(canSearchMessages,
                         "unexpected, you should be able to search");
            MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                    ("[%d] not a good time for this server, try again later",
                     serverIndex));
          }
        } else {
          MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
                  ("[%d] last purge too recent", serverIndex));
        }
      }
    }
    if (folderToPurge && purgeIntervalToUse != 0)
      rv = SearchFolderToPurge(folderToPurge, purgeIntervalToUse);
  }

  // set up timer to check accounts again
  SetupNextPurge();
  return rv;
}

nsresult nsMsgPurgeService::SearchFolderToPurge(nsIMsgFolder* folder,
                                                int32_t purgeInterval) {
  nsresult rv;
  mSearchSession =
      do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  mSearchSession->RegisterListener(this, nsIMsgSearchSession::allNotifications);

  // update the time we attempted to purge this folder
  char dateBuf[100];
  dateBuf[0] = '\0';
  PRExplodedTime exploded;
  PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
  PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%a %b %d %H:%M:%S %Y",
                         &exploded);
  folder->SetStringProperty("curJunkFolderLastPurgeTime",
                            nsDependentCString(dateBuf));
  MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
          ("curJunkFolderLastPurgeTime is now %s", dateBuf));

  nsCOMPtr<nsIMsgIncomingServer> server;
  // We need to get the folder's server scope because imap can have
  // local junk folder.
  rv = folder->GetServer(getter_AddRefs(server));
  NS_ENSURE_SUCCESS(rv, rv);

  nsMsgSearchScopeValue searchScope;
  server->GetSearchScope(&searchScope);

  mSearchSession->AddScopeTerm(searchScope, folder);

  // look for messages older than the cutoff
  // you can't also search by junk status, see
  // nsMsgPurgeService::OnSearchHit()
  nsCOMPtr<nsIMsgSearchTerm> searchTerm;
  mSearchSession->CreateTerm(getter_AddRefs(searchTerm));
  if (searchTerm) {
    searchTerm->SetAttrib(nsMsgSearchAttrib::AgeInDays);
    searchTerm->SetOp(nsMsgSearchOp::IsGreaterThan);
    nsCOMPtr<nsIMsgSearchValue> searchValue;
    searchTerm->GetValue(getter_AddRefs(searchValue));
    if (searchValue) {
      searchValue->SetAttrib(nsMsgSearchAttrib::AgeInDays);
      searchValue->SetAge((uint32_t)purgeInterval);
      searchTerm->SetValue(searchValue);
    }
    searchTerm->SetBooleanAnd(false);
    mSearchSession->AppendTerm(searchTerm);
  }

  // we are about to search
  // create mHdrsToDelete array (if not previously created)
  NS_ASSERTION(mHdrsToDelete.IsEmpty(), "mHdrsToDelete is not empty");

  mSearchFolder = folder;
  return mSearchSession->Search(nullptr);
}

NS_IMETHODIMP nsMsgPurgeService::OnNewSearch() {
  MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("on new search"));
  return NS_OK;
}

NS_IMETHODIMP nsMsgPurgeService::OnSearchHit(nsIMsgDBHdr* aMsgHdr,
                                             nsIMsgFolder* aFolder) {
  NS_ENSURE_ARG_POINTER(aMsgHdr);

  nsCString messageId;
  nsCString author;
  nsCString subject;

  aMsgHdr->GetMessageId(getter_Copies(messageId));
  MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
          ("messageId=%s", messageId.get()));
  aMsgHdr->GetSubject(subject);
  MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
          ("subject=%s", subject.get()));
  aMsgHdr->GetAuthor(getter_Copies(author));
  MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
          ("author=%s", author.get()));

  // double check that the message is junk before adding to
  // the list of messages to delete
  //
  // note, we can't just search for messages that are junk
  // because not all imap server support keywords
  // (which we use for the junk score)
  // so the junk status would be in the message db.
  //
  // see bug #194090
  nsCString junkScoreStr;
  nsresult rv = aMsgHdr->GetStringProperty("junkscore", junkScoreStr);
  NS_ENSURE_SUCCESS(rv, rv);

  MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
          ("junkScore=%s (if empty or != nsIJunkMailPlugin::IS_SPAM_SCORE, "
           "don't add to list delete)",
           junkScoreStr.get()));

  // if "junkscore" is not set, don't delete the message
  if (junkScoreStr.IsEmpty()) return NS_OK;

  if (atoi(junkScoreStr.get()) == nsIJunkMailPlugin::IS_SPAM_SCORE) {
    MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
            ("added message to delete"));
    mHdrsToDelete.AppendElement(aMsgHdr);
  }
  return NS_OK;
}

NS_IMETHODIMP nsMsgPurgeService::OnSearchDone(nsresult status) {
  if (NS_SUCCEEDED(status)) {
    uint32_t count = mHdrsToDelete.Length();
    MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
            ("%d messages to delete", count));

    if (count > 0) {
      MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("delete messages"));
      if (mSearchFolder)
        mSearchFolder->DeleteMessages(
            mHdrsToDelete, nullptr, false /*delete storage*/, false /*isMove*/,
            nullptr, false /*allowUndo*/);
    }
  }
  mHdrsToDelete.Clear();
  if (mSearchSession) mSearchSession->UnregisterListener(this);
  // don't cache the session
  // just create another search session next time we search, rather than
  // clearing scopes, terms etc. we also use mSearchSession to determine if the
  // purge service is "busy"
  mSearchSession = nullptr;
  mSearchFolder = nullptr;
  return NS_OK;
}