summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/src/VirtualFolderWrapper.jsm
blob: 6fc86c8d1eaa16198b8f8f8d64ffd3569a11bed2 (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
/* 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/. */

/*
 * Wrap everything about virtual folders.
 */

const EXPORTED_SYMBOLS = ["VirtualFolderHelper"];

var { MailServices } = ChromeUtils.import(
  "resource:///modules/MailServices.jsm"
);

var VirtualFolderHelper = {
  /**
   * Create a new virtual folder (an actual nsIMsgFolder that did not previously
   *  exist), wrapping it in a VirtualFolderWrapper, and returning that wrapper.
   *
   * If the call to addSubfolder fails (and therefore throws), we will NOT catch
   *  it.
   *
   * @param {string} aFolderName - The name of the new folder to create.
   * @param {nsIMsgFolder} aParentFolder - The folder in which to create the
   *   search folder.
   * @param {nsIMsgFolder[]} aSearchFolders A list of nsIMsgFolders that you
   *   want to use as the sources for the virtual folder OR a string that is
   *   the already '|' delimited list of folder URIs to use.
   * @param {nsIMsgSearchTerms[]} aSearchTerms - The search terms to
   *   use for the virtual folder.
   * @param {boolean} aOnlineSearch Should the search attempt to use the
   *    server's search capabilities when possible and appropriate?
   * @returns {VirtualFolderWrapper} The VirtualFolderWrapper wrapping the
   *   newly created folder. You would probably only want this for its
   *   virtualFolder attribute which has the nsIMsgFolder we created.
   *   Be careful about accessing any of the other attributes, as they will
   *   bring its message database back to life.
   */
  createNewVirtualFolder(
    aFolderName,
    aParentFolder,
    aSearchFolders,
    aSearchTerms,
    aOnlineSearch
  ) {
    let msgFolder = aParentFolder.addSubfolder(aFolderName);
    msgFolder.prettyName = aFolderName;
    msgFolder.setFlag(Ci.nsMsgFolderFlags.Virtual);

    let wrappedVirt = new VirtualFolderWrapper(msgFolder);
    wrappedVirt.searchTerms = aSearchTerms;
    wrappedVirt.searchFolders = aSearchFolders;
    wrappedVirt.onlineSearch = aOnlineSearch;

    let msgDatabase = msgFolder.msgDatabase;
    msgDatabase.summaryValid = true;
    msgDatabase.close(true);

    aParentFolder.notifyFolderAdded(msgFolder);
    MailServices.accounts.saveVirtualFolders();

    return wrappedVirt;
  },

  /**
   * Given an existing nsIMsgFolder that is a virtual folder, wrap it into a
   *  VirtualFolderWrapper.
   *
   * @param {nsIMsgFolder} aMsgFolder - The folder to use.
   */
  wrapVirtualFolder(aMsgFolder) {
    return new VirtualFolderWrapper(aMsgFolder);
  },
};

/**
 * Abstracts dealing with the properties of a virtual folder that differentiate
 *  it from a non-virtual folder.  A virtual folder is an odd duck.  When
 *  holding an nsIMsgFolder that is a virtual folder, it is distinguished by
 *  the virtual flag and a number of properties that tell us the string
 *  representation of its search, the folders it searches over, and whether we
 *  use online searching or not.
 * Virtual folders and their defining attributes are loaded from
 *  virtualFolders.dat (in the profile directory) by the account manager at
 *  startup, (re-)creating them if need be.  It also saves them back to the
 *  file at shutdown.  The most important thing the account manager does is to
 *  create VirtualFolderChangeListener instances that are registered with the
 *  message database service.  This means that if one of the databases for the
 *  folders that the virtual folder includes is opened for some reason (for
 *  example, new messages are added to the folder because of a filter or they
 *  are delivered there), the virtual folder gets a chance to know about this
 *  and update the virtual folder's "cache" of information, such as the message
 *  counts or the presence of the message in the folder.
 * The odd part is that a lot of the virtual folder logic also happens as a
 *  result of the nsMsgDBView subclasses being told the search query and the
 *  underlying folders.  This makes for an odd collaboration of UI and backend
 *  logic.
 *
 * Justification for this class:  Virtual folders aren't all that complex, but
 *  they are complex enough that we don't want to have the same code duplicated
 *  all over the place.  We also don't want to have a loose assembly of global
 *  functions for working with them.  So here we are.
 *
 * Important! Accessing any of our attributes results in the message database
 *  being loaded so that we can access the dBFolderInfo associated with the
 *  database.  The message database is not automatically forgotten by the
 *  folder, which can lead to an (effective) memory leak.  Please make sure
 *  that you are playing your part in not leaking memory by only using the
 *  wrapper when you have a serious need to access the database, and by
 *  forcing the folder to forget about the database when you are done by
 *  setting the database to null (unless you know with confidence someone else
 *  definitely wants the database around and will clean it up.)
 *
 * @param {nsIMsgFolder} aVirtualFolder - Folder to wrap.
 */
function VirtualFolderWrapper(aVirtualFolder) {
  this.virtualFolder = aVirtualFolder;
}
VirtualFolderWrapper.prototype = {
  /**
   * @returns {nsIMsgFolders[]} The list of nsIMsgFolders that this virtual
   *   folder is a search over.
   */
  get searchFolders() {
    return this.dbFolderInfo
      .getCharProperty("searchFolderUri")
      .split("|")
      .sort() // Put folders in URI order so a parent is always before a child.
      .map(uri => MailServices.folderLookup.getOrCreateFolderForURL(uri))
      .filter(Boolean);
  },
  /**
   * Set the search folders that back this virtual folder.
   *
   * @param {string|nsIMsgFolder[]} aFolders - Either a "|"-delimited string of
   *   folder URIs or a list of folders.
   */
  set searchFolders(aFolders) {
    if (typeof aFolders == "string") {
      this.dbFolderInfo.setCharProperty("searchFolderUri", aFolders);
    } else {
      let uris = aFolders.map(folder => folder.URI);
      this.dbFolderInfo.setCharProperty("searchFolderUri", uris.join("|"));
    }
    Services.obs.notifyObservers(this.virtualFolder, "search-folders-changed");
  },

  /**
   * @returns {string} a "|"-delimited string containing the URIs of the folders
   *   that back this virtual folder.
   */
  get searchFolderURIs() {
    return this.dbFolderInfo.getCharProperty("searchFolderUri");
  },

  /**
   * @returns {nsIMsgSearchTerm[]} The list of search terms that define this
   *   virtual folder.
   */
  get searchTerms() {
    return this.searchTermsSession.searchTerms;
  },
  /**
   * @returns {nsIMsgFilterList} A newly created filter with the search terms
   *   loaded into it that define this virtual folder. The filter is apparently
   *   useful as an nsIMsgSearchSession stand-in to some code.
   */
  get searchTermsSession() {
    // Temporary means it doesn't get exposed to the UI and doesn't get saved to
    //  disk.  Which is good, because this is just a trick to parse the string
    //  into search terms.
    let filterList = MailServices.filters.getTempFilterList(this.virtualFolder);
    let tempFilter = filterList.createFilter("temp");
    filterList.parseCondition(tempFilter, this.searchString);
    return tempFilter;
  },

  /**
   * Set the search string for this virtual folder to the stringified version of
   *  the provided list of nsIMsgSearchTerm search terms.  If you already have
   *  a strinigified version of the search constraint, just set |searchString|
   *  directly.
   *
   * @param {string[]} aTerms - a list of search terms
   */
  set searchTerms(aTerms) {
    let condition = "";
    for (let term of aTerms) {
      if (condition) {
        condition += " ";
      }
      if (term.matchAll) {
        condition = "ALL";
        break;
      }
      condition += term.booleanAnd ? "AND (" : "OR (";
      condition += term.termAsString + ")";
    }
    this.searchString = condition;
  },

  /**
   * @returns {string} the set of search terms that define this virtual folder
   *   as a string. You may prefer to use |searchTerms| which converts them
   *   into a list of nsIMsgSearchTerms instead.
   */
  get searchString() {
    return this.dbFolderInfo.getCharProperty("searchStr");
  },
  /**
   * Set the search that defines this virtual folder from a string.  If you have
   *  a list of nsIMsgSearchTerms, you should use |searchTerms| instead.
   *
   * @param {string} aSearchString
   */
  set searchString(aSearchString) {
    this.dbFolderInfo.setCharProperty("searchStr", aSearchString);
  },

  /**
   * @returns {boolean} whether the virtual folder is configured for online search.
   */
  get onlineSearch() {
    return this.dbFolderInfo.getBooleanProperty("searchOnline", false);
  },
  /**
   * Set whether the virtual folder is configured for online search.
   *
   * @param {boolean} aOnlineSearch
   */
  set onlineSearch(aOnlineSearch) {
    this.dbFolderInfo.setBooleanProperty("searchOnline", aOnlineSearch);
  },

  /**
   * @returns {?nsIDBFolderInfo} The dBFolderInfo associated with the virtual
   *   folder directly. Maybe null.  Will cause the message database to be
   *   opened, which may have memory bloat/leak ramifications, so make sure
   *    the folder's database was already going to be opened anyways or that you
   *   call |cleanUpMessageDatabase|.
   */
  get dbFolderInfo() {
    let msgDatabase = this.virtualFolder.msgDatabase;
    return msgDatabase && msgDatabase.dBFolderInfo;
  },

  /**
   * Avoid memory bloat by making the virtual folder forget about its database.
   *  If the database is actually in use (read: someone is keeping it alive by
   *  having references to it from places other than the nsIMsgFolder), the
   *  folder will be able to re-establish the reference for minimal cost.
   */
  cleanUpMessageDatabase() {
    this.virtualFolder.msgDatabase.close(true);
    this.virtualFolder.msgDatabase = null;
  },
};