1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a new, empty filter rules controller.
 26  * @class
 27  * This class represents the filter rules controller. This controller manages
 28  * the filter rules page, which has a button toolbar and a list view of the rules.
 29  *
 30  * @author Conrad Damon
 31  *
 32  * @param {DwtShell}		container		the shell
 33  * @param {ZmPreferencesApp}	prefsApp		the preferences application
 34  * 
 35  * @extends		ZmController
 36  */
 37 ZmFilterRulesController = function(container, prefsApp, prefsView, parent, outgoing) {
 38 
 39 	ZmController.call(this, container, prefsApp);
 40 
 41 	this._prefsView = prefsView;
 42 	this._parent = parent;
 43 
 44 	this._filterRulesView = new ZmFilterRulesView(this._prefsView, this);
 45 
 46 	this._outgoing = Boolean(outgoing);
 47 
 48 	this._buttonListeners = {};
 49 	this._buttonListeners[ZmOperation.ADD_FILTER_RULE] = new AjxListener(this, this._addListener);
 50 	this._buttonListeners[ZmOperation.EDIT_FILTER_RULE] = new AjxListener(this, this._editListener);
 51 	this._buttonListeners[ZmOperation.REMOVE_FILTER_RULE] = new AjxListener(this, this._removeListener);
 52 	this._buttonListeners[ZmOperation.RUN_FILTER_RULE] = new AjxListener(this, this._runListener);
 53 	this._progressController = new ZmProgressController(container, prefsApp);
 54 
 55 	// reset community name since it gets its value from a setting
 56 	ZmFilterRule.C_LABEL[ZmFilterRule.C_COMMUNITY] = ZmMsg.communityName;
 57 };
 58 
 59 ZmFilterRulesController.prototype = new ZmController();
 60 ZmFilterRulesController.prototype.constructor = ZmFilterRulesController;
 61 
 62 ZmFilterRulesController.prototype.toString =
 63 function() {
 64 	return "ZmFilterRulesController";
 65 };
 66 
 67 ZmFilterRulesController.prototype.isOutgoing =
 68 function() {
 69 	return this._outgoing;
 70 };
 71 
 72 /**
 73  * Gets the filter rules view, which is comprised of a toolbar and a list view.
 74  * 
 75  * @return	{ZmFilterRulesView}		the filter rules view
 76  */
 77 ZmFilterRulesController.prototype.getFilterRulesView =
 78 function() {
 79 	return this._filterRulesView;
 80 };
 81 
 82 /**
 83  * Initializes the controller.
 84  * 
 85  * @param	{ZmToolBar}	toolbar		the toolbar
 86  * @param	{ZmListView}	listView		active list view
 87  * @param   {ZmListView}    listView        not active list view
 88  */
 89 ZmFilterRulesController.prototype.initialize =
 90 function(toolbar, listView, notActiveListView) {
 91 	// always reset the the rules to make sure we get the right one for the *active* account
 92 	this._rules = AjxDispatcher.run(this._outgoing ? "GetOutgoingFilterRules" : "GetFilterRules");
 93 
 94 	if (toolbar) {
 95 		var buttons = this.getToolbarButtons();
 96 		for (var i = 0; i < buttons.length; i++) {
 97 			var id = buttons[i];
 98 			if (this._buttonListeners[id]) {
 99 				toolbar.addSelectionListener(id, this._buttonListeners[id]);
100 			}
101 		}
102 		this._resetOperations(toolbar, 0);
103 	}
104 
105 	if (notActiveListView) {
106 		this._notActiveListView = notActiveListView;
107 		notActiveListView.addSelectionListener(new AjxListener(this, this._listSelectionListener));
108 		notActiveListView.addActionListener(new AjxListener(this, this._listActionListener));
109 		this.resetListView(0);
110 	}
111 	
112 	if (listView) {
113 		this._listView = listView;
114 		listView.addSelectionListener(new AjxListener(this, this._listSelectionListener));
115 		listView.addActionListener(new AjxListener(this, this._listActionListener));
116 		this.resetListView(0);
117 	}
118 	
119 };
120 
121 ZmFilterRulesController.prototype.getRules =
122 function() {
123 	if (!this._rules)
124 		this._rules = AjxDispatcher.run(this._outgoing ? "GetOutgoingFilterRules" : "GetFilterRules");
125 	return this._rules;
126 };
127 
128 ZmFilterRulesController.prototype.getToolbarButtons =
129 function() {
130 	var ops = [
131 		ZmOperation.ADD_FILTER_RULE,
132 		ZmOperation.SEP,
133 		ZmOperation.EDIT_FILTER_RULE,
134 		ZmOperation.SEP,
135 		ZmOperation.REMOVE_FILTER_RULE
136 	];
137 
138 	// bug: 42903 - disable running filters in offline for now
139 	if (!appCtxt.isOffline) {
140 		ops.push(ZmOperation.SEP, ZmOperation.RUN_FILTER_RULE);
141 	}
142 
143 	return ops;
144 };
145 
146 ZmFilterRulesController.prototype.resetListView =
147 function(selectedIndex) {
148 	if (!this._listView) { return; }
149 
150 	var respCallback = new AjxCallback(this, this._handleResponseSetListView, [selectedIndex]);
151 	this._rules.loadRules(true, respCallback);  //bug 37339 - filters don't show newly created filter
152 };
153 
154 ZmFilterRulesController.prototype._handleResponseSetListView =
155 function(selectedIndex, result) {
156 	this._listView.set(result.getResponse().clone());
157 	this._notActiveListView.set(result.getResponse().clone());
158 	var rule = this._rules.getRuleByIndex(selectedIndex || 0);
159 	if (rule && rule.active) {
160 		this._listView.setSelection(rule);
161 	}
162 	else if (rule) {
163 		this._notActiveListView.setSelection(rule);
164 	}
165 };
166 
167 /**
168  * Handles left-clicking on a rule. Double click opens up a rule for editing.
169  *
170  * @param	{DwtEvent}	ev		the click event
171  * 
172  * @private
173  */
174 ZmFilterRulesController.prototype._listSelectionListener =
175 function(ev) {
176 	var listView = this.getListView();
177 	if (ev.detail == DwtListView.ITEM_DBL_CLICKED) {
178 		this._editListener(ev);
179 	} else {
180 		var tb = this._filterRulesView.getToolbar();
181 		this._resetOperations(tb, listView.getSelectionCount(), listView.getSelection());
182 	}
183 };
184 
185 ZmFilterRulesController.prototype._listActionListener =
186 function(ev) {
187 	var listView = this.getListView();
188 	var actionMenu = this.getActionMenu();
189 	this._resetOperations(actionMenu, listView.getSelectionCount(), listView.getSelection());
190 	actionMenu.popup(0, ev.docX, ev.docY);
191 };
192 
193 /**
194  * Gets the action menu.
195  * 
196  * @return	{ZmActionMenu}		the action menu
197  */
198 ZmFilterRulesController.prototype.getActionMenu =
199 function() {
200 	if (!this._actionMenu) {
201 		this._initializeActionMenu();
202 		var listView = this.getListView();
203 		this._resetOperations(this._actionMenu, 0, listView.getSelection());
204 	}
205 	return this._actionMenu;
206 };
207 
208 // action menu: menu items and listeners
209 ZmFilterRulesController.prototype._initializeActionMenu =
210 function() {
211 	if (this._actionMenu) { return; }
212 
213 	var menuItems = this._getActionMenuOps();
214 	if (menuItems) {
215 		var params = {
216 			parent:this._shell,
217 			menuItems:menuItems,
218 			context:this._getMenuContext(),
219 			controller:this
220 		};
221 		this._actionMenu = new ZmActionMenu(params);
222 		this._addMenuListeners(this._actionMenu);
223 	}
224 };
225 
226 ZmFilterRulesController.prototype._getActionMenuOps =
227 function() {
228 	var ops = [
229 		ZmOperation.EDIT_FILTER_RULE,
230 		ZmOperation.REMOVE_FILTER_RULE
231 	];
232 
233 	// bug: 42903 - disable running filters in offline for now
234 	if (!appCtxt.isOffline) {
235 		ops.push(ZmOperation.RUN_FILTER_RULE);
236 	}
237 
238 	ops.push(ZmOperation.SEP,
239 			ZmOperation.MOVE_UP_FILTER_RULE,
240 			ZmOperation.MOVE_DOWN_FILTER_RULE
241 	);
242 
243 	return ops;
244 };
245 
246 /**
247  * Returns the context for the action menu created by this controller (used to create
248  * an ID for the menu).
249  */
250 ZmFilterRulesController.prototype._getMenuContext =
251 function() {
252 	return this._app && this._app._name;
253 };
254 
255 ZmFilterRulesController.prototype._addMenuListeners =
256 function(menu) {
257 	var menuItems = menu.opList;
258 	for (var i = 0; i < menuItems.length; i++) {
259 		var menuItem = menuItems[i];
260 		if (this._buttonListeners[menuItem]) {
261 			menu.addSelectionListener(menuItem, this._buttonListeners[menuItem], 0);
262 		}
263 	}
264 	menu.addPopdownListener(this._menuPopdownListener);
265 };
266 
267 /**
268 * The "Add Filter" button has been pressed.
269 *
270 * @ev		[DwtEvent]		the click event
271 */
272 ZmFilterRulesController.prototype._addListener =
273 function(ev) {
274 	var listView = this.getListView();
275 	if (!listView) { return; }
276 	this.handleBeforeFilterChange(new AjxCallback(this, this._popUpAdd));
277 };
278 
279 ZmFilterRulesController.prototype.handleBeforeFilterChange =
280 function(okCallback, cancelCallback) {
281 	if (this._outgoing && (appCtxt.getSettings().getSetting(ZmSetting.SAVE_TO_SENT).getValue()===false || ZmPref.getFormValue(ZmSetting.SAVE_TO_SENT)===false)) {
282 		var dialog = appCtxt.getConfirmationDialog();
283 		if (!this._saveToSentMessage) {
284 			var html = [];
285 			var i = 0;
286 			html[i++] = "<table cellspacing=0 cellpadding=0 border=0><tr><td valign='top'>";
287 			html[i++] = AjxImg.getImageHtml("Warning_32");
288 			html[i++] = "</td><td class='DwtMsgArea'>";
289 			html[i++] = ZmMsg.filterOutgoingNoSaveToSentWarning;
290 			html[i++] = "</td></tr></table>";
291 			this._saveToSentMessage = html.join("");
292 		}
293 		var handleSaveToSentYesListener = new AjxListener(this, this._handleSaveToSentYes, [okCallback]);
294 		var handleSaveToSentNoListener = new AjxListener(this, this._handleSaveToSentNo, [okCallback]);
295 		
296 		dialog.popup(this._saveToSentMessage, handleSaveToSentYesListener, handleSaveToSentNoListener, cancelCallback);
297 		dialog.setTitle(AjxMsg.warningMsg);
298 	} else {
299 		if (okCallback)
300 			okCallback.run();
301 	}
302 };
303 
304 ZmFilterRulesController.prototype._handleSaveToSentYes =
305 function(callback) {
306 	var settings = appCtxt.getSettings();
307 	var setting = settings.getSetting(ZmSetting.SAVE_TO_SENT);
308 	ZmPref.setFormValue(ZmSetting.SAVE_TO_SENT, true);
309 	if (!setting.getValue()) {
310 		setting.setValue(true);
311 		settings.save([setting], callback);
312 	} else {
313 		if (callback)
314 			callback.run();
315 	}
316 };
317 
318 ZmFilterRulesController.prototype._handleSaveToSentNo =
319 function(callback) {
320 	if (callback)
321 		callback.run();
322 };
323 
324 ZmFilterRulesController.prototype._popUpAdd =
325 function() {
326 	var listView = this.getListView();
327 	var sel = listView.getSelection();
328 	var refRule = sel.length ? sel[sel.length - 1] : null;
329 	appCtxt.getFilterRuleDialog().popup(null, false, refRule, null, this._outgoing);
330 };
331 
332 /**
333 * The "Edit Filter" button has been pressed.
334 *
335 * @ev		[DwtEvent]		the click event
336 */
337 ZmFilterRulesController.prototype._editListener =
338 function(ev) {
339 	var listView = this.getListView();
340 	if (!listView) { return; }
341 
342 	var sel = listView.getSelection();
343 	appCtxt.getFilterRuleDialog().popup(sel[0], true, null, null, this._outgoing);
344 };
345 
346 /**
347 * The "Delete Filter" button has been pressed.
348 *
349 * @ev			[DwtEvent]		the click event
350 */
351 ZmFilterRulesController.prototype._removeListener =
352 function(ev) {
353 	var listView = this.getListView();
354 	if (!listView) { return; }
355 	var sel = listView.getSelection();
356 	var rule = sel[0];
357 	//bug:16053 changed getYesNoCancelMsgDialog to getYesNoMsgDialog
358 	var ds = this._deleteShield = appCtxt.getYesNoMsgDialog();
359 	ds.reset();
360 	ds.registerCallback(DwtDialog.NO_BUTTON, this._clearDialog, this, this._deleteShield);
361 	ds.registerCallback(DwtDialog.YES_BUTTON, this._deleteShieldYesCallback, this, rule);
362 	var msg = AjxMessageFormat.format(ZmMsg.askDeleteFilter, AjxStringUtil.htmlEncode(rule.name));
363 	ds.setMessage(msg, DwtMessageDialog.WARNING_STYLE);
364 	ds.popup();
365 };
366 
367 ZmFilterRulesController.prototype._runListener =
368 function(ev) {
369 	// !!! do *NOT* get choose folder dialog from appCtxt since this one has checkboxes!
370 	if (!this._chooseFolderDialog) {
371 		AjxDispatcher.require("Extras");
372 		this._chooseFolderDialog = new ZmChooseFolderDialog(appCtxt.getShell());
373 	}
374 	this._chooseFolderDialog.reset();
375 	this._chooseFolderDialog.registerCallback(DwtDialog.OK_BUTTON, this._runFilterOkCallback, this, this._chooseFolderDialog);
376 
377 	// bug 42725: always omit shared folders
378 	var omit = {};
379 	var tree = appCtxt.getTree(ZmOrganizer.FOLDER);
380 	var children = tree.root.children.getArray();
381 	for (var i = 0; i < children.length; i++) {
382 		var child = children[i];
383 		if (child.type == ZmOrganizer.FOLDER && child.isRemote()) {
384 			omit[child.id] = true;
385 		}
386 	}
387 
388 	var params = {
389 		treeIds:		[ZmOrganizer.FOLDER],
390 		title:			ZmMsg.chooseFolder,
391 		overviewId:		this.toString() + (this._outgoing ? "_outgoing":"_incoming"),
392 		description:	ZmMsg.chooseFolderToFilter,
393 		skipReadOnly:	true,
394 		hideNewButton:	true,
395 		treeStyle:		DwtTree.CHECKEDITEM_STYLE,
396 		appName:		ZmApp.MAIL,
397 		omit:			omit
398 	};
399 	this._chooseFolderDialog.popup(params);
400 
401 	var foundForwardAction;
402 	var listView = this.getListView();
403 	var sel = listView && listView.getSelection();
404 	for (var i = 0; i < sel.length; i++) {
405 		if (sel[i].actions[ZmFilterRule.A_NAME_FORWARD]) {
406 			foundForwardAction = true;
407 			break;
408 		}
409 	}
410 
411 	if (foundForwardAction) {
412 		var dialog = appCtxt.getMsgDialog();
413 		dialog.setMessage(ZmMsg.filterForwardActionWarning);
414 		dialog.popup();
415 	}
416 };
417 
418 ZmFilterRulesController.prototype._runFilterOkCallback =
419 function(dialog, folderList) {
420 	dialog.popdown();
421 	var listView = this.getListView();
422 	var filterSel = listView && listView.getSelection();
423 	if (!(filterSel && filterSel.length)) {
424 		return;
425 	}
426 
427 	// Bug 78392: We need the selection sorted
428 	if (filterSel.length > 1) {
429 		var list = this._listView.getList().getArray();
430 		var selectedIds = {}, sortedSelection = [];
431 		for (var i=0; i<filterSel.length; i++) {
432 			selectedIds[filterSel[i].id] = true;
433 		}
434 		for (var i=0; i<list.length; i++) {
435 			if (selectedIds[list[i].id]) {
436 				sortedSelection.push(list[i]);
437 			}
438 		}
439 		filterSel = sortedSelection;
440 	}
441 
442 	var work = new ZmFilterWork(filterSel, this._outgoing);
443 
444 	this._progressController.start(folderList, work);
445 
446 };
447 
448 /**
449  * runs a specified list of filters
450  * 
451  * @param container     {DwtControl} container reference
452  * @param filterSel     {Array} array of ZmFilterRule
453  * @param isOutgoing    {Boolean} 
454  */
455 ZmFilterRulesController.prototype.runFilter = 
456 function(container, filterSel, isOutgoing) {
457 	var work = new ZmFilterWork(filterSel, isOutgoing);
458 	this._progressController.start(container, work);
459 };
460 
461 /**
462 * The user has agreed to delete a filter rule.
463 *
464 * @param rule	[ZmFilterRule]		rule to delete
465 */
466 ZmFilterRulesController.prototype._deleteShieldYesCallback =
467 function(rule) {
468 	this._rules.removeRule(rule);
469 	this._clearDialog(this._deleteShield);
470 	this._resetOperations(this._filterRulesView.getToolbar(), 0);
471 };
472 
473 /**
474 * The "Move Up" button has been pressed.
475 *
476 * @param	ev		[DwtEvent]		the click event
477 */
478 ZmFilterRulesController.prototype.moveUpListener =
479 function(ev) {
480 	var listView = this.getListView();
481 	if (!listView) { return; }
482 
483 	var sel = listView.getSelection();
484 	this._rules.moveUp(sel[0]);
485 };
486 
487 /**
488 * The "Move Down" button has been pressed.
489 *
490 * @ev		[DwtEvent]		the click event
491 */
492 ZmFilterRulesController.prototype.moveDownListener =
493 function(ev) {
494 	var listView = this.getListView();
495 	if (!listView) { return; }
496 
497 	var sel = listView.getSelection();
498 	this._rules.moveDown(sel[0]);
499 };
500 
501 /**
502 * Resets the toolbar button states, depending on which rule is selected.
503 * The list view enforces single selection only. If the first rule is selected,
504 * "Move Up" is disabled. Same for last rule and "Move Down". They're both
505 * disabled if there aren't at least two rules.
506 *
507 * @param parent		[ZmButtonToolBar]	the toolbar
508 * @param numSel		[int]				number of rules selected (0 or 1)
509 * @param sel		[Array]				list of selected rules
510 */
511 ZmFilterRulesController.prototype._resetOperations =
512 function(parent, numSel, sel) {
513 	var numRules = this._rules.getNumberOfRules();
514 	if (numSel == 1) {
515 		parent.enableAll(true);
516 	} else {
517 		parent.enableAll(false);
518 		parent.enable(ZmOperation.ADD_FILTER_RULE, true);
519 		if (numSel > 1) {
520 			parent.enable(ZmOperation.RUN_FILTER_RULE, true);
521 		}
522 	}
523 
524 	if (numRules == 0) {
525 		parent.enable(ZmOperation.EDIT_FILTER_RULE, false);
526 		parent.enable(ZmOperation.REMOVE_FILTER_RULE, false);
527 	}
528 };
529 
530 ZmFilterRulesController.prototype.getListView =
531 function(){
532 	if (this._listView && this._notActiveListView) {
533 		var activeSel = this._listView.getSelection();
534 		var notActiveSel = this._notActiveListView.getSelection();
535 		if (!AjxUtil.isEmpty(activeSel)) {
536 			return this._listView;
537 		}
538 		else if (!AjxUtil.isEmpty(notActiveSel)) {
539 			return this._notActiveListView;
540 		}
541 	}
542     return this._listView;
543 };
544 
545 
546 
547 /**
548  * class that holds the work specification (in this case, filtering specific filters. Keeps track of progress stats too.
549  * an instance of this is passed to ZmFilterRulesController to callback for stuff specific to this work. (template pattern, I believe)
550  * @param filterSel
551  * @param outgoing
552  */
553 ZmFilterWork = function(filterSel, outgoing) {
554 	this._filterSel = filterSel;
555 	this._outgoing = outgoing;
556 	this._totalNumMessagesAffected = 0;
557 
558 };
559 
560 /**
561  * return the summary message when finished everything.
562  */
563 ZmFilterWork.prototype.getFinishedMessage =
564 function(messagesProcessed) {
565 	if (messagesProcessed) {
566 		return AjxMessageFormat.format(ZmMsg.filterRuleApplied, [messagesProcessed, this._totalNumMessagesAffected]);
567 	}
568 	else {
569 		return AjxMessageFormat.format(ZmMsg.filterRuleAppliedBackground, [this._totalNumMessagesAffected]);
570 	}
571 };
572 
573 /**
574  * return the progress so far summary.
575  */
576 ZmFilterWork.prototype.getProgressMessage =
577 function(messagesProcessed) {
578 	return AjxMessageFormat.format(ZmMsg.filterRunInProgress, [messagesProcessed, this._totalNumMessagesAffected]);
579 };
580 
581 /**
582  * return the finished dialog title.
583  */
584 ZmFilterWork.prototype.getFinishedTitle =
585 function(messagesProcessed) {
586 	return AjxMessageFormat.format(ZmMsg.filterRunFinished);
587 };
588 
589 /**
590  * return the progress dialog title.
591  */
592 ZmFilterWork.prototype.getProgressTitle =
593 function(messagesProcessed) {
594 	return AjxMessageFormat.format(ZmMsg.filterRunInProgressTitle);
595 };
596 
597 
598 /**
599  * do the work. (in this case apply filters). Either msgIds or query should be set but not both.
600  * @param msgIds {String} chunk of message ids to do the work on.
601  * @param query {String} query to run filter against
602  * @param callback
603  */
604 ZmFilterWork.prototype.doWork =
605 function(msgIds, query, callback) {
606 	var filterSel = this._filterSel;
607 	var soapDoc = AjxSoapDoc.create(this._outgoing ? "ApplyOutgoingFilterRulesRequest" : "ApplyFilterRulesRequest", "urn:zimbraMail");
608 	var filterRules = soapDoc.set("filterRules", null);
609 	for (var i = 0; i < filterSel.length; i++) {
610 		var rule = soapDoc.set("filterRule", null, filterRules);
611 		rule.setAttribute("name", filterSel[i].name);
612 	}
613 	var noBusyOverlay = false;
614 	if (msgIds) {
615 		var m = soapDoc.set("m");
616 		m.setAttribute("ids", msgIds.join(","));
617 	}
618 	else {
619 		soapDoc.set("query", query);
620 		noBusyOverlay = true;
621 	}
622 
623 	var params = {
624 		soapDoc: soapDoc,
625 		asyncMode: true,
626 		noBusyOverlay: noBusyOverlay,
627 		callback: (new AjxCallback(this, this._handleRunFilter, [callback]))
628 	};
629 	appCtxt.getAppController().sendRequest(params);
630 };
631 
632 /**
633  * private method - gets the result of the filter request, and keeps track of total messages affected.
634  * @param callback
635  * @param result
636  */
637 ZmFilterWork.prototype._handleRunFilter =
638 function(callback, result) {
639 	var r = result.getResponse();
640 	var resp = this._outgoing ? r.ApplyOutgoingFilterRulesResponse : r.ApplyFilterRulesResponse;
641 	var num = (resp && resp.m && resp.m.length)
642 		? (resp.m[0].ids.split(",").length) : 0;
643 	this._totalNumMessagesAffected += num;
644 	callback.run();
645 };
646 
647 
648