/* Copyright 2009 Palm, Inc.  All rights reserved. */

var MessageAssistant = Class.create({
	initialize : function(targetEmail, folderId, focusStage, detailsObj) {
		this.data = { id: targetEmail, senderDetails:{} }; // data.id
		// This data is used to pre-render info in case the mailservice isn't able to respond quickly enough
		if (detailsObj) {
			this.data.prerenderData = true;
			this.data.displayName = detailsObj.displayName;
			this.data.summary = detailsObj.summary;
			this.data.timeStamp = detailsObj.timeStamp;
			this.data.flags = detailsObj.flags;
			this.data.priority = detailsObj.priority;
			this.folderFilter = detailsObj.folderFilter;
			this.itemIndex = detailsObj.offset;
		}
		this.folderId = folderId;
		this.account = {};
		this.gotFirstResponse = false;
		this.bodyLeftOffset = 0;
		this.transition = null;
		this.waitingForMessageBodyTimeout = undefined;
		this.pendingComposeActions = [];
		this.attachmentDLProgress = {
			lastUpdate:0,
			progress:{},
			subs:{},
			clearSubscription: function(id) {
				try {
					this.subs[id].cancel();
					delete (this.subs[id]);
					delete (this.progress[id]);
				} catch (e) {
					Mojo.Log.logException(e, "clearSubscription");
				}
			}
		};
		this.attachmentAutoOpened = false;
		
		// rendering flags
		this.delayActivate = true;
		this.renderedMessageBody = false;
		
		this.parentAssistant = AppAssistant.getSceneAssistant("list");		
		this.boundShowHideRecipients = this.showHideRecipients.bind(this);
		this.boundGotoNextEmailNewer = this.gotoNextEmail.bind(this, 'newer');
		this.boundGotoNextEmailOlder = this.gotoNextEmail.bind(this, 'older');
		this.boundHandleInviteResponseAccept = this.handleInviteResponse.bind(this, 'accept');
		this.boundHandleInviteResponseTentative = this.handleInviteResponse.bind(this, 'tentative');
		this.boundHandleInviteResponseDecline = this.handleInviteResponse.bind(this, 'decline');
		this.boundHandleInviteResponseRemove = this.handleInviteResponse.bind(this, 'remove');
		this.boundHandleLinkClicked = this.handleLinkClicked.bind(this);
		this.boundHandleInlineImageSaved = this.handleInlineImageSaved.bind(this);
		this.boundHandleWebViewSingleTap = this.handleWebViewSingleTap.bind(this);
		this.boundHandleDOMClicked = this.handleDOMClicked.bind(this);
		this.boundHandleMessageDownloadRetry = this.handleMessageDownloadRetry.bind(this);
		
		this.boundHandleAttachmentTapped = this.handleAttachmentTapped.bind(this);
		this.boundHandleShowHideAttachmentList = this.showHideAttachmentList.bind(this);
		
		if (focusStage === true) {
			this.focusStageTimer = this.focusEmailStage.bind(this).delay(0.6);
		}
	},

	setup: function() {
		this.setupMessage(); // get message details and setup message webview widget
		
		// setup menu	
		this.cmdMenuModel = {
				visible:true,
				items: [
					{label:$L('Reply'),     icon:'reply',     command:'reply'},
					{label:$L('Reply all'), icon:'reply-all', command:'replyAll'},
					{label:$L('Forward'),   icon:'forward-email', command:'forward'},
					{label:$L('Delete'),    icon:'delete',    command:'delete'}
				]};
		this.controller.setupWidget(Mojo.Menu.commandMenu, undefined, this.cmdMenuModel);
		
		this.markUnreadMenuItem =     {label:MessageAssistant.kAppMenuMarkUnread, shortcut:'k', command:'mark-unread'};
		this.markSetFlagMenuItem =    {label:MessageAssistant.kAppMenuSetFlag, shortcut:'g', command:'flag'};
		this.showRecipientsMenuItem = {label:MessageAssistant.kAppMenuShowRecipients, shortcut:'s', command:'show-recipients'};
		
		this.appMenuAttrs = {
			omitDefaultItems: true
		};

		// Add "Copy All" to the standard edit menu.
		var emailEditItems = {
			label: $L('Edit'),
			items: [
				Mojo.Menu.selectAllItem,
				Mojo.Menu.cutItem,
				Mojo.Menu.copyItem,
				{label: $L('Copy All'), command: 'copyAll'},
				Mojo.Menu.pasteItem
			]
		};

		this.appMenuModel = {
			visible: true,
			label: $L('Chat view menu'),
			items: [
				emailEditItems,
				this.markUnreadMenuItem,
				this.markSetFlagMenuItem,
				{label:$L('Move to Folder...'), shortcut:'m', command:'move'},
				this.showRecipientsMenuItem,
				Mojo.Menu.prefsItem,
				Mojo.Menu.helpItem
			]
		};
		this.controller.setupWidget(Mojo.Menu.appMenu, this.appMenuAttrs, this.appMenuModel);

		this.recipsDrawer = { openProperty: false };
		this.controller.setupWidget('email_recipients_drawer', {unstyled:true, modelProperty:'openProperty'}, this.recipsDrawer);
		this.recipsDrawer.element = this.controller.get('email_recipients_drawer');
		
		// SETUP EVENTS
		this.emailHeaderBlock = this.controller.get('email_header_block');
		this.emailPicturesBlock = this.controller.get('email_pictures_list');
		this.recipientController = this.controller.get('email_recipients_controller');
		
		this.messageTarget = this.controller.get('readview-main');
		this.messageTarget.observe('mousedown', this.hideAttachmentList.bind(this), true);

		this.controller.get('email-readview-attachments-list').observe(Mojo.Event.tap, this.boundHandleAttachmentTapped);
		this.attachmentsShowElem = this.controller.get('show-hide-multi-attachments');
		this.attachmentsShowElem.observe(Mojo.Event.tap, this.boundHandleShowHideAttachmentList);
		
		this.emailPicturesBlock.observe(Mojo.Event.tap, this.handleImageTapped.bind(this));
		// stop gesture events on pictures so they don't go to the webview widget
		this.emailPicturesBlock.observe('gesturestart', function(event) { event.stop(); }, false);
		this.emailPicturesBlock.observe('gesturechange', function(event) { event.stop(); }, false);
		this.emailPicturesBlock.observe('gestureend', function(event) { event.stop(); }, false);

		this.fromElement = this.controller.get('email_from');
		this.messageTarget.observe(Mojo.Event.tap, this.boundHandleDOMClicked);
		this.fromElement.observe(Mojo.Event.tap, this.handleSenderTap.bind(this));
		this.controller.get('email_recipients').observe(Mojo.Event.tap, this.handleRecipientListSelect.bind(this));

		this.boundUpdateRecipientStatus = this.updateRecipientStatus.bind(this);
		Mojo.Event.listen(this.controller.stageController.document, Mojo.Event.activate, this.boundUpdateRecipientStatus);

		Mojo.Event.listen(this.controller.getSceneScroller(), Mojo.Event.scrollStarting, this.addAsScrollListener.bind(this));
		
		this.subjectElement = this.controller.get('email_subject');
		this.readViewContainer = this.controller.get('email-readview-content-container');
		
	},

	cleanup: function() {
		if (this.waitingForMessageBodyTimeout !== undefined) {
			clearTimeout(this.waitingForMessageBodyTimeout);
			this.waitingForMessageBodyTimeout = undefined;
		}

		if (this.focusStageTimer !== undefined) {
			clearTimeout(this.focusStageTimer);
		}
		
		if (this.messageSubscription) {
			this.messageSubscription.cancel();
		}

		this.webview.removeEventListener(Mojo.Event.webViewUrlRedirect, this.boundHandleLinkClicked, false);
		this.webview.removeEventListener(Mojo.Event.webViewMimeNotSupported, this.boundHandleLinkClicked, false);
		this.webview.removeEventListener(Mojo.Event.webViewMimeHandoff, this.boundHandleLinkClicked, false);
		this.webview.removeEventListener(Mojo.Event.webViewImageSaved, this.boundHandleInlineImageSaved, false);
		this.webview.removeEventListener('singletap', this.boundHandleWebViewSingleTap, true);
		
		
		this.messageTarget.stopObserving(Mojo.Event.tap, this.boundHandleDOMClicked);
		
		Mojo.Event.stopListening(this.controller.stageController.document, Mojo.Event.activate, this.boundUpdateRecipientStatus);
		//  This currently does nothing. Commenting out
		//  Message.closeMessage(this.controller, this.data.id);
	},

	aboutToActivate: function(callback) {
		if (this.delayActivate === true) {
			this.readyToActivateCallback = callback;
		} else {
			callback();
		}
	},

	activate: function() {
  		// If the scene is invalid (usually because the underlying account was deleted), 
		// just pop the scene and no more.
  		if (this.popOnActivate === true) {
			this.controller.stageController.popScene();
			return;
		}
		
		// prerenderData is only set if the data was supplied from the list assistant. If the
		// service already send a response, it will not include the prerenderData property.
 		if (this.data.prerenderData === true) {
			// about to activate has timed out. Render what we can
			// while waiting for the rest of the response
			this.prerenderMessage(this.data);
		}

		// save the current scene controller physics parameters
		var scroller = this.controller.getSceneScroller();
		this.savedFlickSpeed = scroller.mojo.kFlickSpeed;
		this.savedFlickRatio = scroller.mojo.kFlickRatio;
		scroller.mojo.updatePhysicsParameters({flickSpeed: 0.12, flickRatio: 0.2});

		// If the scene is ready to activate and it needs to be focused, do that now.
		if (this.focusStageTimer !== undefined) {
			clearTimeout(this.focusStageTimer);
			this.focusEmailStage();
		}
	},

	deactivate: function() {
		// restore the current scene controller physics parameters
		var scroller = this.controller.getSceneScroller();
		scroller.mojo.updatePhysicsParameters({flickSpeed: this.savedFlickSpeed, flickRatio: this.savedFlickRatio});
	},

	addAsScrollListener: function(event) {
		event.scroller.addListener(this);
	},

	moved: function() {
		var scrollOffset = this.messageTarget.viewportOffset();
		if (this.bodyLeftOffset === scrollOffset.left) {
			// don't perform calculation again if we don't have to
			return;
		} else if (this.webview !== undefined) {
			this.bodyLeftOffset = scrollOffset.left;
			// Get the width of the browseradapter since that should tell the truth about its width
			var webviewWidth = this.webview.down().getWidth();
			//console.log("offsets: ov x=" + scrollOffset.left + ", w=" + this.emailHeaderBlock.getWidth() + ", wh=" + webviewWidth);
			
			if (scrollOffset.left > 0) {
				scrollOffset.left = 0;
			} else {
				var rightExtent = (this.emailHeaderBlock.getWidth() - webviewWidth);
				if (scrollOffset.left < rightExtent) {
					scrollOffset.left = rightExtent;
				}
			}
			
			// move the header block & the attached pictures block so they appear to not scroll horizontally
			var leftOffset = -scrollOffset.left + 'px';
			this.emailHeaderBlock.setStyle({ 'left': leftOffset });
			this.emailPicturesBlock.setStyle({ 'left': leftOffset });
		}
	},

	/**
	 * 
	 * storageId is variable. Is object id in case of recipient objects, email address in case of senders
	 * @param {Object} resp
	 */
	displayContactAvatarAndPresence: function(baseId, storageId, resp) {
		var nameId = baseId + "-name";
		var avatarId = baseId + "-photo";
		var presenceId = baseId + "-presence";		

		// If this person isn't in contact, give him ID=0. This is done in case the 
		// contact was deleted, in which case the old settings need to be cleared.
		if (!resp.record) {
			resp.record = { id:0 };
		}

		if (baseId === "email-sender") {
			this.data.fromID = resp.record.id;
			
			// cache update for message sender
			if (storageId) {
				MessageAssistant.recipientCache.push(storageId, resp);
			} else if (resp.match !== undefined && resp.match !== null && resp.match.value !== undefined) {
				MessageAssistant.recipientCache.push(resp.match.value, resp);
			}

			//<Reminder Info>			
			if (resp.record && resp.record.reminder) {
				if (this.contactReminder === undefined) {
 					this.contactReminder = new ContactReminder();
				}
				this.contactReminder.displayReminder(resp);
			}
		} else {
			this.controller.get(storageId).writeAttribute('contactid', resp.record.id);
		}

		var nameElem = this.controller.get(nameId);
		if (nameElem) {
			if (resp.record.displayText) {	
				nameElem.update(resp.record.displayText);
			} else if (Contact.recordHasNames(resp.record)) {
				nameElem.update(Contact.formatPersonName(resp.record));
			} 
		}

		if (resp.record.pictureLocSquare) {
			Mojo.Log.info("Displaying sender's picture ", resp.record.pictureLocSquare);
			var imgElem = this.controller.get(avatarId);
			imgElem.src = "file://" + resp.record.pictureLocSquare;
			var imgFrame = imgElem.up('.from-photo');
			if (imgFrame !== undefined) {
				imgFrame.show();
			}
			imgElem.parentNode.parentNode.parentNode.addClassName("has-avatar-icon"); 
		}

		if (resp.record) {
			if (resp.record.imAvailability !== undefined &&
			    resp.record.imAvailability !== null &&
				resp.record.imAvailability !== IMName.NO_PRESENCE) {
				Mojo.Log.info("imAvailability = ", resp.record.imAvailability);
				var imPresence;
				switch (resp.record.imAvailability) {
	                case IMName.BUSY:
	                    imPresence = 'status-busy';
	                    break;
	                case IMName.IDLE:
	                    imPresence = 'status-idle';
	                    break;
	                case IMName.ONLINE:
	                    imPresence = 'status-available';
	                    break;
					default:
	                    imPresence = 'status-offline';
				}
				var imgElem = this.controller.get(presenceId);
				imgElem.addClassName(imPresence);
				imgElem.show();
			}
		}
	},

	batchDisplayContactAvatarAndPresence: function(originalDetails, resp) {
		// peel these off and feed to the single method
		var respToDisplay;
				
		// cache responses only if details do not take up more than half of the max cache size
		var cacheRecips = originalDetails.length < (MessageAssistant.recipientCache.maxSize / 2);
		
		var responses = {}; 
		while (resp.matches.length > 0) {
			// Make first pass over responses, so we can determine missing persons below
			// use local responses hash for actual contact display
			respToDisplay = resp.matches.pop();
			if (cacheRecips) {
				MessageAssistant.recipientCache.push(respToDisplay.match.value, respToDisplay);
			}
			responses[respToDisplay.match.value] = respToDisplay;
		}
	
		var personDetails;
		for (var i = 0, len = originalDetails.length; i < len; ++i) {
			personDetails = originalDetails[i];
			if (!responses[personDetails.address]) {
				// use dummy response
				if (cacheRecips) {
					MessageAssistant.recipientCache.push(personDetails.address, MessageAssistant.kDummyResponse);
				}
				this.displayContactAvatarAndPresence('recip-'+personDetails.id, personDetails.id, MessageAssistant.kDummyResponse);
			} else {
				this.displayContactAvatarAndPresence('recip-'+personDetails.id, personDetails.id, responses[personDetails.address]);
			}
		}
	},

	folderAndAccountDetails: function(resp) {
		// this gives us the following properties folder, account, login, protocol: EAS|IMAP|POP3}
		this.account = resp;
		
		// Add the email id to the account info because it will be used by the moveto scene
		this.account.emailId = this.data.id;
		var assistant = Mojo.Controller.getAppController().assistant;
		assistant.notificationAssistant.clear(resp.account, resp.folder, this.data.id);
		assistant.clearDebounce('m'+this.data.id);
	},

	updateRecipientStatus: function() {
		EmailRecipient.getDetails(this.controller, this.data.senderDetails.address, this.displayContactAvatarAndPresence.bind(this, 'email-sender', null));

		if (this.data.onlyRecipients !== undefined) {
			var theThis = this;
			this.data.onlyRecipients.each(function(r) {
				EmailRecipient.getDetails(theThis.controller, r.address, theThis.displayContactAvatarAndPresence.bind(theThis, 'recip-'+r.id, r.id));
			});
		}
	},

	handleSenderTap: function(event) {
		this.showSenderContactDetails(event);
	},
	
	/**
	 * command handler. Show sender's contact details when selected
	 * @param {Object} event
	 */
	showSenderContactDetails: function(event) {
		if (this.data.fromID) {
			EmailRecipient.launchContactDetails(this.controller, this.data.fromID);
		} else {
			var displayName = this.data.displayName;
			// if the display name is the email address it isn't really the person's name
			if (displayName === this.data.senderDetails.address) {
				displayName = "";
			}
			EmailRecipient.addToContacts(this.controller, this.data.senderDetails.address, displayName);
		}
	},

	handleRecipientListSelect: function(event) {
		var targetRow = this.controller.get(event.target);
		if (!targetRow.hasClassName('email-recipient')) {
			targetRow = targetRow.up('div.email-recipient');
		}

		if (targetRow) {
			var contactId = targetRow.readAttribute('contactid');
			if (contactId && contactId > 0) {
				EmailRecipient.launchContactDetails(this.controller, contactId);
			} else {
				var address = targetRow.readAttribute('address');
				var displayName = targetRow.readAttribute('displayname');
				displayName = displayName.escapeHTML();
				EmailRecipient.addToContacts(this.controller, address, displayName);
			}
		}
	},

	showHideRecipients: function(event) {
		var isOpen = !this.recipsDrawer.element.mojo.getOpenState();
		this.recipsDrawer.element.mojo.setOpenState(isOpen);
		this.controller.get('email_recipients_compressed_list').toggle();
		this.controller.get('email_recipients_compressed_count').toggle();
		
		if (isOpen) {
			this.showRecipientsMenuItem.label = MessageAssistant.kAppMenuHideRecipients;
		} else {
			this.showRecipientsMenuItem.label = MessageAssistant.kAppMenuShowRecipients;
		}
	},

	/**
	 * Response handler for message subscription. Calls all necessary methods for
	 * proper message display  
	 * @param {Object} resp
	 */
	messageDetailsUpdated: function(resp) {
		if (this.gotFirstResponse === false) {
			this.gotFirstResponse = true;
			this.renderMessage(resp);
		} else {
			Mojo.Log.info("renderMessage: waiting for message body. isFullyLoaded=", resp.isFullyLoaded);
			if (resp.isFullyLoaded == "true") {
				if (this.waitingForMessageBodyTimeout !== undefined) {
					clearTimeout(this.waitingForMessageBodyTimeout);
					this.waitingForMessageBodyTimeout = undefined;
				}
				// Add the email body to the stored data model
				this.data.textURI = resp.textURI;
				this.data.text = resp.text;
				this.data.isFullyLoaded = resp.isFullyLoaded;
				this.data.isHtml = resp.isHtml;
				if (!this.renderedMessageBody) {
					this.renderMessageBody(this.data);
				}
			}

			// POP doesn't know it has attachments until it has downloaded the full envelope
			// so use the new attachment list if one there doesn't already exist
			if ((!this.data.attachments || this.data.attachments.length === 0) &&
			    resp.attachments && !this.attachmentsRendered && EmailFlags.hasAttachment(resp.flags)) {
				this.data.attachments = resp.attachments;
				this.renderMessageAttachments(resp.attachments);
			}
		}
	},

	// This is a special function that is only called when the scene is first launched
	// and activate() occurs before the mailservice has time to respond with the full
	// email data.
	prerenderMessage: function(resp) {
		this.data.prerenderData = false; // only want to prerender email once
		Mojo.Log.info("prerenderMessage ", resp.id);

		if ((resp.flags & EmailFlags.flaggedBit) !== 0) {
			resp.flagged = "starred";
		}

		this.renderSubjectAndSender(resp);
		this.prerendered = true;
	},
	
	/** common method for rendering subject and sender from response.
	 * Called by prerender or renderMessage
	 * @param {Object} resp
	 */
	renderSubjectAndSender: function (resp) {
		//convert the timestamp into a Date
		this.calcResponseTimestamp(resp);
		
		// subject and sender can be supplied from the list widget directly.
		// If a filter was applied, these values are pre-prepped with a span 
		// underlining the matched query phrase. Only escapeHTML for normal cases
		
		if (resp.displayName) {
			resp.displayName = resp.displayName.escapeHTML();
		}
		if (this.data.summary == null || this.data.summary.length === 0) {
			this.data.summary = Message.kNoSubjectString;
		} else {
			// unescape and then re-escape, since we seem to be having problems
			// with prior escaping
			this.data.summary = this.data.summary.unescapeHTML().escapeHTML(); 
		}
		
		resp.summary = PalmSystem.runTextIndexer(resp.summary); 
		var content = Mojo.View.render({template: 'message/message_from', object:resp});
		this.fromElement.update(content);
		//subject
		resp.priority = Email.getPriorityClass(resp.priority);
		content = Mojo.View.render({template: 'message/message_subject', object: resp});
		this.subjectElement.update(content);
		// buttons now available through template 
		this.controller.get('previous_email').observe(Mojo.Event.tap, this.boundGotoNextEmailNewer);
		this.controller.get('next_email').observe(Mojo.Event.tap, this.boundGotoNextEmailOlder);
		this.subjectUpdated = true;
		
		// set flagged icon if flag bit has been set
		// bitwise and. Not a typo.
		if (this.data.flags & EmailFlags.flaggedBit) {
			this.controller.get('email_favorite').addClassName("starred");
		}
	},
	
	calcResponseTimestamp: function(resp) {
		if (resp.formattedDate) {
			return; // already done
		}
		var theDate = new Date(parseInt(resp.timeStamp, 10));
		resp.formattedDate = Mojo.Format.formatDate(theDate, {date:'medium', time:'short'});
		resp.meridiem = "";
	},

	renderMessage: function(resp) {
		var content;
		Mojo.Log.info("renderMessage ", resp.id);
		this.data = resp;
		if (!this.data.senderDetails)
			this.data.senderDetails = {};
		
		if (!this.prerendered) {
			// need to make sure sender divs are in place
			// for contact avatar display
			this.renderSubjectAndSender(resp);
		}
		
		// Clear notification for this message if such a notification exists.
		this.folderAndAccountDetails({
			"folder": resp.folder,
			"account": resp.account,
			"login": resp.login,
			"protocol": resp.protocol
		});

		// Very first thing to do with recipients is fix them up for the address picker.
		EmailRecipient.addAddressPickerFields(resp.recipients);
		this.renderMessageRecipients.bind(this,resp).defer(); // expensive, and not as important as message body
		
		//text -- if it is fully loaded then render it (need to check for string "true" since that's what's coming in json).
		if (this.data.isFullyLoaded == "true" && !this.renderedMessageBody) {
			this.renderMessageBody(this.data);
		} else {
			this.waitForMessageBody();
		}

		this.renderInviteBlocks(resp);
		
		// set message flags
		if ((resp.flags & EmailFlags.flaggedBit) !== 0) {
			resp.flagged = "starred";
			this.markSetFlagMenuItem.label = MessageAssistant.kAppMenuClearFlag;
		} else {
			this.markSetFlagMenuItem.label = MessageAssistant.kAppMenuSetFlag;
		}
		
		// Get details about the next and previous emails to know where the user can navigate.
		// NOTE: this needs to be after message_subject is rendered
		this.handleNextMessagesResponse(resp.nextMessages);

		// Attachments - some may be in the HTML body, so only show the attachments area if there
		// is something in the attachments array
		var attachments = resp.attachments;
		if (attachments && !this.attachmentsRendered && EmailFlags.hasAttachment(this.data.flags)) {
			this.renderMessageAttachments(attachments);
		} else {
			// If no attachments, make sure old UI is cleaned out.
			this.controller.get('email-readview-attachments-block').hide();
			this.emailPicturesBlock.update("");
		}

		// Set the "read" flag if need be
		if (!EmailFlags.isRead(resp.flags)) {
			Message.setRead(resp.id, true);
			this.updateMessageInParentScene();
		}
		
		this.adjustWhitespace();
		
		// Now that we have the details, fire the readyToActivate callback
		if (this.delayActivate === true) {
			this.delayActivate = false;
			// Got the message details so say we're ready to render
			if (this.readyToActivateCallback !== undefined) {
				this.readyToActivateCallback();
				this.readyToActivateCallback = undefined;
			}
		}

		if (this.transition !== null) {
			// transitioning between next/prev message
			this.transition.run();
			this.transition.cleanup();
			this.transition = null;
		}

	},
	
	adjustWhitespace: function() {
		// Ensure the whiteness is at least tall enough to fill the screen. 
		var po = this.readViewContainer.positionedOffset();
		var minHeight = (this.controller.window.innerHeight - po.top) + 'px';
		this.readViewContainer.setStyle({'min-height':minHeight});
	},
	
	renderInviteBlocks: function(resp) {
		if (EmailFlags.isMeetingRequest(resp.flags)) {
			var invite = new Object();
			if (resp.whenStart) {
				var startDate = new Date(parseInt(resp.whenStart, 10));
				var dateFormat = $L('EEE, MMM d');
				var whenObject = {
					date:      Mojo.Format.formatDate(startDate, {date:dateFormat}),
					startTime: Mojo.Format.formatDate(startDate, {time:'short'}),
					endTime:   Mojo.Format.formatDate(new Date(parseInt(resp.whenEnd, 10)), {time:'short'})
				};
				invite.when = $L("#{date}, #{startTime} - #{endTime}").interpolate(whenObject);				
			} else {
				// Service used to format the date. That doesn't work for too many reasons
				invite.when = $L("#{when}").interpolate(resp); //TODO parse this and put in local date format
			}
			if (resp.busyStatus === 0) {
				// Maybe change the style here
				invite.conflict = $L("Conflicts with another event");
			}
			invite.where = $L("#{where}").interpolate(resp).escapeHTML();

			content = Mojo.View.render({template: 'message/meeting_invitation', object: invite});
			this.controller.get('email-readview-invitations').update(content);

			this.controller.get('invite-accept').observe(Mojo.Event.tap, this.boundHandleInviteResponseAccept);
			this.controller.get('invite-tentative').observe(Mojo.Event.tap, this.boundHandleInviteResponseTentative);
			this.controller.get('invite-decline').observe(Mojo.Event.tap, this.boundHandleInviteResponseDecline);
		}
		else if (EmailFlags.isMeetingCancel(resp.flags)) {
			content = Mojo.View.render({template: 'message/meeting_cancellation', object: {}});
			this.controller.get('email-readview-invitations').update(content);
			this.controller.get('invite-remove').observe(Mojo.Event.tap, this.boundHandleInviteResponseRemove);
		}
		else {
			this.controller.get('email-readview-invitations').update("");
		}
	},

	renderMessageRecipients: function(resp) {
		Mojo.Log.info("Starting to render message recipients.");
		var recips = resp.recipients;
		this.contactAvatarLookupNeeded = false;
		this.data.onlyRecipients = [];
		this.data.senderDetails = {};
		var content = null;
		if (recips) {
			var recipTypes;
			if (EmailFlags.isMeetingType(resp.flags)) {
				recipTypes = [ $L("From"), $L("Required"), $L("Optional"), $L("Bcc"), $L("From") ];
			} else {
				recipTypes = [ $L("From"), $L("To"), $L("Cc"), $L("Bcc"), $L("From") ];
			}

			var theThis = this;
			// closure time
			var prevRecp = {}; // This is used to determine when to display To/Cc/Bcc divider
			var recipientAddresses = new Array(); // used to supply the batch reverse-lookup  
			var pendingDisplayCalls = new Array(); // this is used to queue displayCalls for cached recipients. 
			recips.each(function(r) {
				if(recipTypes[r.role] !== undefined) {
					if (r.role === EmailRecipient.roleTo || r.role === EmailRecipient.roleCc || r.role === EmailRecipient.roleBcc) {
						r.roleStr = recipTypes[r.role];
						if (r.role != prevRecp.role) {
							r.rowDisplayRole = "show";
							r.hasDivider = "has-divider";
							prevRecp.hideLast = "last";
						}
						theThis.data.onlyRecipients.push(r);
						prevRecp = r;
						if (MessageAssistant.recipientCache.isCached(r.address)) {
							// queue bound function and execute later. Otherwise will execute before dom is ready
							pendingDisplayCalls.push(theThis.displayContactAvatarAndPresence.bind(theThis, 'recip-' + r.id, r.id, MessageAssistant.recipientCache.get(r.address)));
						} else {
							recipientAddresses.push(r.address);
						}
					}
					// roleReplyTo can overwrite the senderDetails because it takes precidence over roleFrom
					else if (r.role === EmailRecipient.roleReplyTo) {
						theThis.data.senderDetails = r;
					}
					// roleFrom should not overwrite the senderDetails because roleReplyTo takes precidence
					else if (r.role === EmailRecipient.roleFrom && theThis.data.senderDetails.address === undefined) {
						theThis.data.senderDetails = r;
					}
				}
			});
			 if (this.data.senderDetails.address !== undefined) {
			 	// sender details are a special case. we pass null for storageID if we have the 
				// sender cached already, or the address if we do not
                if (MessageAssistant.recipientCache.isCached(this.data.senderDetails.address)) {
					this.displayContactAvatarAndPresence('email-sender', null, MessageAssistant.recipientCache.get(this.data.senderDetails.address));
                }
                else {
                    // Get the contact ID for the sender of the email
					// special case for displayContactAvatar
                    EmailRecipient.getDetails(this.controller, this.data.senderDetails.address, this.displayContactAvatarAndPresence.bind(this, 'email-sender', this.data.senderDetails.address));
                }
            }

			if (this.data.onlyRecipients.length > 0) {
				// if we have message recipients to show, build from the template
				var recipElem = this.controller.get('email_recipients');
				content = Mojo.View.render({
				    collection: this.data.onlyRecipients,
				    template: 'message/message_recips'
				});
				recipElem.update(content);
				
				var recipsSummary = { count:0, to:[], cc:[], bcc:[] };
				this.data.onlyRecipients.each(function(r) {
					if (r.role === EmailRecipient.roleTo) {
						recipsSummary.to.push(r.displayName);
						recipsSummary.count++;
					} else if (r.role === EmailRecipient.roleCc) {
						recipsSummary.cc.push(r.displayName);
						recipsSummary.count++;
					} else if (r.role === EmailRecipient.roleBcc) {
						recipsSummary.bcc.push(r.displayName);
						recipsSummary.count++;
					}
				});

				if (recipsSummary.count > 0) {
					recipsSummary.displayTo = "none";
					recipsSummary.displayCc = "none";
					recipsSummary.displayBcc = "none";
					if (recipsSummary.to.length > 0 && recipsSummary.cc.length > 0) {
						recipsSummary.displayTo = "inline";
						recipsSummary.displayCc = "inline";
						recipsSummary.toList = recipsSummary.to.join(', ');
						recipsSummary.ccList = recipsSummary.cc.join(', ');
					} else if (recipsSummary.to.length > 0) {
						recipsSummary.displayTo = "inline";
						recipsSummary.toList = recipsSummary.to.join(', ');
					} else if (recipsSummary.cc.length > 0) {
						recipsSummary.displayCc = "inline";
						recipsSummary.ccList = recipsSummary.cc.join(', ');
					} else if (recipsSummary.bcc.length > 0) {
						recipsSummary.displayBcc = "inline";
						recipsSummary.bccList = recipsSummary.bcc.join(', ');
					}

					if (recipsSummary.count === 1) {
						recipsSummary.recipientCount = $L("1 recipient");
					} else {
						recipsSummary.recipientCount = $L("#{count} recipients").interpolate(recipsSummary);
					}

					content = Mojo.View.render({template: 'message/message_recips_compressed', object: recipsSummary });
					this.recipientController.update(content);
					// Observe the row since that is the entire height and width of the tappable area
					this.recipientController.up(".palm-row").observe(Mojo.Event.tap, this.boundShowHideRecipients, true);
				}
			}
		} // recipients dom finally ready
		
		if (pendingDisplayCalls.length != this.data.onlyRecipients.length) {
			// batch get and update recipients if not available in cache already
			EmailRecipient.batchGetDetails(this.controller, recipientAddresses, this.batchDisplayContactAvatarAndPresence.bind(this, this.data.onlyRecipients));
		} else {
			this.recipientAddresses = []; // don't need to keep around
		}
		
		while (pendingDisplayCalls.length > 0) {
			// fire off queued displays
			pendingDisplayCalls.pop().apply();
		}
		
		// If there were no recipients, then display the default "no recips" 
		if (content === null) {
			Mojo.Log.info("displaying 'Only BCC recipients'");
			var renderParams = {
				template: 'message/message_recips_compressed',
				object: {onlyBccRecipient: $L("Only BCC recipients"), displayTo: "none", displayCc: "none", displayBcc: "none" }
			};
			this.recipientController.update(Mojo.View.render(renderParams));
		}
	},

	processTextProperty: function(data) {
		var msgText = palmGetResource(data.text, true);
		
		// If it is an html email, it starts with "<!DOCTYPE" or "<html" some simple html tag
		if (!data.isHtml) {
			//console.log("******* NO MATCH *********")
			// Plain text needs to replace the carriage returns with html line-break tags
			data.text = msgText.escapeHTML().gsub("\n","<br>");
		} else {
			//console.log("******* MATCH *********")
			data.text = msgText.stripScripts();
		}
	},

	renderMessageBody: function(data) {
		if (data.isFullyLoaded != "true") {
			Mojo.Log.error("renderMessageBody aborted because data.isFullyLoaded == ", data.isFullyLoaded)
			return;
		}
		
		this.hideNoBodyUI();
		try {
			Mojo.Log.breadcrumb("rendering URI" + data.textURI);
			this.webview.mojo.openURL("file://" + data.textURI);
		} catch (e) {
			Mojo.Log.logException(e, 'renderMessageBody openUrl');	
		}
			
		// If the service didn't supply the text to use for replying to an email, request it now.
		if (!data.text) {
			var processTextProperty = this.processTextProperty;
			Message.getMessageText(this.controller, data.id,
					function(resp) {
						data.text = resp.text;
						processTextProperty(data);
					},
					function() { Mojo.Log.error("messageFileText failed for textURI", data.textURI) }
			);
		} else {
			// Defer b/c this can be done after the email is rendered
			this.processTextProperty(data);		
		}
		
		// check to see whether pending compose actions should be executed
		if (!this.emailLoaded) {
			this.emailLoaded = true;
			this.handlePendingComposeActions();
		}
				
		this.webview.mojo.focus();
		this.renderedMessageBody = true;
	},

	renderMessageAttachments: function(attachments) {
		// Show the div containing the attachments. For multiple attachments, also
		// show the expand/collapse controller
		this.controller.get('email-readview-attachments-block').show();

		var picturesList = [];
		var names = undefined;
		var fixupAttachment = function(a){
			a.displayName = a.displayName.escapeHTML();
			if (names === undefined) {
				names = a.displayName.substring(0); // copy of the name 
			}
			else {
				names += ", " + a.displayName;
			}
			// Icons
			Attachments.processFileObject(a);
			// Picture to be displayed
			if (a.iconType == "type-image") {
				var extension = a.extension.toLowerCase();
				if (extension === ".jpg" || extension === ".jpeg" || extension === ".png") {
					a.fixeduri = "file:///var/luna/data/extractfs/" + a.uri + ":0:0:320:240:3";
				}
				else {
					a.fixeduri = a.uri;
				}
				picturesList.push(a);
			}
			else {
				a.displayImage = "display:none";
			}
			
			// Download %
			if (a.uri) {
				a.display = "display: none;";
				a.download = "";
			}
			else {
				a.download = "show-download-icon";
				a.display = "display: none;";
			}
		};
		attachments.each(function(attachment) { fixupAttachment(attachment); });

		// Only show the special "this file and N others" if there's more than 1 attachment 
		if (attachments.length > 1) {
			// Setup the attachment compressed view
			var content = Mojo.View.render({
				object: { displayName:names, count:attachments.length },
				template: 'message/message_attachments_compressed'
			});
			this.attachmentsShowElem.update(content);
		}
		
		content = Mojo.View.render({
			collection: attachments,
			template: 'message/message_attachments'
		});
		this.controller.get('email-readview-attachments-list').update(content);

		// Attachment pictures
		if (picturesList.length > 0) {
			content = Mojo.View.render({
				collection: picturesList,
				template: 'message/message_pictures'
			});
			this.emailPicturesBlock.update(content);
		} else {
			this.emailPicturesBlock.update("");
		}

		// Convert any downloaded audio attachments to AudioTag controls
		attachments.each(function(a) {
			if (a.uri !== undefined) {
				this.setupAudioTag(a);
			}
		}.bind(this));		

		//
		// Now that the contents of the list are ready, compute heights and setup the "drawer" 
		// 
		this.attachmentList = this.controller.get('attachlist-list1');
		this.attachmentList.setStyle({height:'auto'}); // make sure the height below is the full height
		this.attachmentListHeight = parseInt(this.attachmentList.getHeight(), 10);

		// If the list contains only 1 item so it isn't considered as "shown"
		this.attachmentListShown = (attachments.length > 1);
		if (this.attachmentListShown) {
			// Initially hide the attachments list
			this.attachmentListShown = false;
			this.attachmentList.setStyle({'height':'46px'});
			this.attachmentList.hide();
			this.attachmentsShowElem.show();
		} else {
			this.attachmentsShowElem.hide();
			this.attachmentList.show();
		}
		
		this.attachmentsRendered = true;
	},

	setupAudioTag: function(a) {
		var success = false;
		if (a.uri !== undefined && a.mimeType.startsWith("audio")) {
			var audioElement = this.controller.get("progress_" + a.id);
			try {
				audioElement.show();

				var audioTag = AudioTag.extendElement(audioElement, this.controller, a.uri);
				audioTag.autoplay = false;
				this.controller.get("adetails_" + a.id).hide();
				this.controller.get("download-progress-wrapper_" + a.id).hide();
				//this.controller.get("file-icon_" + a.id).show(); // file icon currently removed
				success = true;
			} catch (e) {
				audioElement.hide();
			}
		}
		return success;
	},

	waitForMessageBody: function() {
		// If the transport can't retrieve the message in a reasonable amount of time, error out
		this.waitingForMessageBodyTimeout = this.waitMessageError.bind(this).delay(45);		
		this.showNoBodyUI();
	},

	waitMessageError: function(resp) {
		if (this.messageSubscription) {
			this.messageSubscription.cancel();
			this.messageSubscription = null;
		}
		Mojo.Log.error("waitMessageError", Object.toJSON(resp));
		
		// Waiting for message body means the message exists, but the body
		// isn't yet on device. If the error occured before even waiting for
		// the body, then popScene out to the previous scene because this
		// email doesn't even exists anymore.   
		if (this.waitingForMessageBodyTimeout === undefined) {
			// This should only occur if the email ID doesn't exist so go back to the previous scene
			Mojo.Log.error("popping the message scene because message doesn't exists. ID=", this.data.id);
			this.controller.stageController.popScene(resp);
		} else {
			clearTimeout(this.waitingForMessageBodyTimeout);
			this.waitingForMessageBodyTimeout = undefined;
			this.hideNoBodyUI();

			// Display error text
			this.messageTimedOut = true;
			this.showDownloadErrorUI();
			this.controller.get('retry-message-download').observe(Mojo.Event.tap, this.boundHandleMessageDownloadRetry);
		}
	},

	showNoBodyUI: function() {
		var nobodyDiv = this.controller.get('email_no_body')
		nobodyDiv.show();
		this.controller.instantiateChildWidgets(nobodyDiv);
		this.controller.get('email_no_body_spinner').mojo.start();
	},

	hideNoBodyUI: function() {
		this.controller.get('email_no_body_spinner').mojo.stop();
		this.controller.get('email_no_body').hide();
	},
	
	showDownloadErrorUI: function() {
		var errorDiv = this.controller.get('email_error_text');
		errorDiv.update(Mojo.View.render({template: 'message/failed_to_download_err', object:{}}));
		errorDiv.show();
	},
	
	hideDownloadErrorUI: function() {
		var errorDiv = this.controller.get('email_error_text');
		errorDiv.hide();
	},
	
	handleMessageDownloadRetry: function() {
		// Hide message error download text and stop observing the retry button
		// tap event.
		this.messageTimedOut = undefined;
		this.hideDownloadErrorUI();
		this.controller.get('retry-message-download').stopObserving(Mojo.Event.tap, this.boundHandleMessageDownloadRetry);
		
		this.waitForMessageBody();
		this.enableMessageSubscription();
	},

	handleToggleFavorites: function(event) {
		// Flip the flagged bit
		var value = true;
		this.data.flags = this.data.flags ^ EmailFlags.flaggedBit;
		if ((this.data.flags & EmailFlags.flaggedBit) == 0) {
			this.controller.get('email_favorite').removeClassName("starred");
			this.data.flagged = "";
			this.markSetFlagMenuItem.label = MessageAssistant.kAppMenuSetFlag;
			value = false;
		} else {
			this.controller.get('email_favorite').addClassName("starred");
			this.data.flagged = "starred";
			this.markSetFlagMenuItem.label = MessageAssistant.kAppMenuClearFlag;
		}
		//console.log("Setting flagged for " + this.data.id + " to " + value);
		Message.setFlagged(this.data.id, value);
	},

	handleAttachmentTapped: function(event) {
		var targetRow = this.controller.get(event.target);
		var listDiv = targetRow;
		if (!listDiv.hasClassName('attachment-info')) {
			listDiv = listDiv.up('div.attachment-info');
		}

		if (listDiv) {
			var attachmentUri = listDiv.getAttribute('x-uri');
			var attachmentMimeType = listDiv.getAttribute('x-mimetype');
			if (attachmentUri) {
				this.openAttachment(listDiv);
			} else if (event.target.className === "download-cancel") {
				this.stopAttachmentDownload(listDiv.id);
				
				if (--this.attachmentDownloadCount === 0)
					this.attachmentAutoOpened = false;
			} else {
				// Stop the timeout
				if (this.hideTimeout) {
					clearTimeout(this.hideTimeout);
					this.hideTimeout = null;
				}
				this.startAttachmentDownload(listDiv.id);
				
				if (this.attachmentDownloadCount === undefined)
					this.attachmentDownloadCount = 1;
				else
					this.attachmentDownloadCount++;
			}
		}
	},

	handleImageTapped: function(event) {
		var imgElem = this.controller.get(event.target.id);
		var uri = imgElem.getAttribute('x-uri');
		this.controller.stageController.pushScene('imageview', uri);
	},
	
	openAttachment: function(listDiv) {
		var attachmentUri = listDiv.getAttribute('x-uri');
		var attachmentMimeType = listDiv.getAttribute('x-mimetype');

		if (attachmentUri) {
			if (attachmentMimeType.startsWith("audio")) {
				// Do nothing because the AudioTag widget handles taps for audio files
			} else if (attachmentMimeType.startsWith("image")) {
				this.controller.stageController.pushScene('imageview', attachmentUri);
			} else {
				if (attachmentUri.startsWith("/")) {
					attachmentUri = "file://" + attachmentUri;
				}
				//Message.launchAttachment(this.controller, attachmentUri, this.error.bind(this));
				Message.getResourceType(this.controller, attachmentUri, attachmentMimeType, this.gotResourceType.bind(this), this.useInternalResourceHandler.bind(this, attachmentMimeType));
			}
		}
	},

	/*
	 * Callback function for getResourceType
	 */
	gotResourceType: function(payload) {
		//Check if this is launchable
		if(payload.returnValue && payload.appIdByExtension) {
			Message.launchAttachments(this.controller,payload.uri, payload.appIdByExtension, payload.mimeByExtension);			
		} else {
			this.useInternalResourceHandler(payload.mimeByExtension, payload);
		}
	},

	useInternalResourceHandler: function(mimeType, response) {
		var type = Attachments.getIconTypeFromMimeType(mimeType);
		if (!type) {
			try {
				if (response.uri !== undefined) {
					var extensionIndex = response.uri.lastIndexOf('.');
					if (extensionIndex > 0) {
						var extension = response.uri.substring(extensionIndex + 1).toLowerCase();
						type = Attachments.getIconTypeFromExtension(extension);
					}
				}
			} catch (e) {
				Mojo.Log.logException(e, "MessageAssistant.useInternalResourceHandler");
			}
		}

		if (type === "type-image") {
			this.controller.stageController.pushScene('imageview', response.uri);
		} else {
			Mojo.Log.error("Email - Attachment can't be opened!");
			this.controller.showAlertDialog({
				onChoose: function(value) {},
				message: $L("Cannot find an application which can open this file."),
				choices: [
					{label:$L('OK'), value:'dismiss', type:'alert'}
				]
			});
		}
	},

	startAttachmentDownload: function(id) {
		Mojo.Log.info("start attachment download", id);
		var progressbar = this.controller.get('progress_' + id);
		if (progressbar.visible()) {
			Mojo.Log.info("ignoring tap because attachment is already downloading");
		} else {
			this.attachmentDLProgress.subs[id] = Message.loadAttachment(id, this.attachmentDownloadProgress.bind(this), this.attachmentError.bind(this, id));

			// This makes the progressbar show up so the user gets immediate feedback. 
			this.controller.get('file_size_' + id).hide(); // showing the progress bar so hide the file size		
			this.controller.get('progress_' + id).show(); // ensures the progress bar is shown
			// Set the initial progress to 0%
			this.attachmentUpdateProgressbar(id, 0);
		}
	},

	stopAttachmentDownload: function(id) {
		Mojo.Log.info("stop attachment download", id);
		this.attachmentDLProgress.clearSubscription(id);
		Message.cancelLoadAttachment(this.controller, id);

		// This makes the progressbar show up so the user gets immediate feedback. 
		this.controller.get('file_size_' + id).show();		
		this.controller.get('progress_' + id).hide();
		// Set the initial progress to 0%
		this.attachmentUpdateProgressbar(id, 0);
	},

	handleInviteResponse: function(response, event) {
		Mojo.Log.info("handleInviteResponse ", response);
		var notificationAssistant = Mojo.Controller.getAppController().assistant.notificationAssistant;
		notificationAssistant.handleNotification({ type:"general", message:$L("Sending invitation response") });
		Message.inviteResponse(this.data, response);
		this.controller.stageController.popScene();
	},

	attachmentError: function(id, err) {
		Mojo.Log.error("handleError ", err.errorText);
		this.stopAttachmentDownload(id);
		--this.attachmentDownloadCount;
		
		var that = this;
		var errorText;
		if (err.errorText && err.errorText.length > 0) {
			errorText = $L("Error downloading file. Server reports: #{errorText}").interpolate(err).escapeHTML();
		} else {
			errorText = $L("Error while downloading file.");
		}
		this.controller.showAlertDialog({
			onChoose: function(value) {},
			title: $L("Unable To Download File"),
			message: errorText,
			choices: [ {label:$L('OK'), value:'dismiss', type:'alert'} ]
		});
	},

	handleAttachmentDetails: function(attachment) {
		Mojo.Log.info("handleAttachmentDetails ", attachment.id);

		// Update the uri for this attachment now that we got one.
		var attch = undefined;
		this.data.attachments.each(function(a) {
			if (a.id == attachment.id) {
				a.uri = attachment.uri;
				attch = a;
				$break;
			}
		});

		// For some reason the id needs to be a string so using toString().
		var elem = this.controller.get((attachment.id).toString());
		var newUri = attachment.uri;
		elem.writeAttribute('x-uri', newUri);
		elem.writeAttribute('x-mimetype', attachment.mimeType);

		var audioSetup = this.setupAudioTag(attachment);
		if (audioSetup) {
			// The height of the inline audio player is different so need to recalc the list height
			this.attachmentList.setStyle({height:'auto'}); // make sure the height below is the full height
			this.attachmentListHeight = parseInt(this.attachmentList.getHeight(), 10);
			
			// we reuse the 'progress_' div for the audio player, so do not hide

		} else {
			var imgElem = this.controller.get('picture_'+attachment.id);
			if (imgElem) {
				Mojo.Log.info("set img src=", newUri);
				imgElem.writeAttribute('x-uri', newUri);
				// Following uri uses extractfs to scale the image to fit 320x240.
				// This fixes problems with extremely large images causing the UI to chug.
				Attachments.processFileObject(attachment);
				var extension = attachment.extension.toLowerCase();
				if (extension === ".jpg" || extension === ".jpeg" || extension === ".png") {
					imgElem.src = "file:///var/luna/data/extractfs/"+newUri+":0:0:320:240:3";
				} else {
					imgElem.src = newUri;
				}

				var thumbnail = elem.down('.readview-image-thumbnail');
				if (thumbnail) {
					thumbnail.src = "file:///var/luna/data/extractfs/"+newUri+":0:0:31:31:4";
					thumbnail.show();
				}
			}
			this.controller.get('download_icon_' + attachment.id).hide(); // remove the download arrow from the icon
			this.controller.get('progress_' + attachment.id).hide(); // ensures the progress bar is no longer shown
			this.controller.get('file_size_' + attachment.id).show(); // since progress bar is gone, show the file size		
		}
		
		if (!this.attachmentAutoOpened && this.shouldAutoOpenAttachment(attch)) {
			this.attachmentAutoOpened = true;
			// auto open known type attachments 
			Mojo.Log.info("Auto opening attachment %s", attch.id);
			this.openAttachment(elem);
		}
		if (--this.attachmentDownloadCount === 0) {
			this.attachmentAutoOpened = false;
		}
	},
	
	shouldAutoOpenAttachment: function(attachment) {
		switch(attachment.iconType) {
			case 'type-image':
			case 'txt':
			case 'vcard':
			case 'type-word':
			case 'pdf':
			case 'xls':
			case 'ppt':
				return true;
			default:
				return false;
		}
	},
	
	attachmentDownloadProgress: function(info) {
		// If the object doesn't contain a 'id' property, it isn't valid download progress
		if (info.id) {
			if (info.status == Message.ATTACHMENT_LOAD_COMPLETED_EVENT) {
				Mojo.Log.info("attachmentDownloadProgress complete for id:", info.id);
				this.attachmentDLProgress.clearSubscription(info.id);
				this.attachmentUpdateProgressbar(info.id, 100);

				// Final step to fully download is to get details of this attachment, including the URI.
				Message.getAttachmentDetails(this.controller, info.id, this.handleAttachmentDetails.bind(this), this.attachmentError.bind(this, info.id));
			}
			else if (info.status == Message.ATTACHMENT_LOAD_PROGRESS_EVENT) {
				// The transports can be rather aggressive about sending progress notifications so
				// defend against that by only updating the UI periodically
				var now = new Date().getTime();
				if (now < this.attachmentDLProgress.lastUpdate + 200) {
					this.attachmentDLProgress.progress[info.id] = info.progress;
				} else {
					this.attachmentDLProgress.lastUpdate = now;
					var that = this;
					if (info.progress !== undefined) {
						this.attachmentDLProgress.progress[info.id] = info.progress;
					}
					var progressObj = this.attachmentDLProgress.progress;
					Object.keys(progressObj).each(function(id) {
						var progress = progressObj[id];
						Mojo.Log.info("attachmentDownloadProgress id: ", id, ", progress: ", progress);
						that.attachmentUpdateProgressbar(id, progress);
					});
				}
			}
		}
	},
	
	attachmentUpdateProgressbar: function(id, percent) {
		var progressGroup = this.controller.get('progress_' + id);
		if (progressGroup) {
			var totalWidth = 2.48; // = 248 / 100%
			var progressWidth = Math.round(totalWidth * percent);
			progressGroup.down("div.download-progress").setStyle({width:progressWidth+10+"px"});
			var backgrndWidth = Math.round(totalWidth * (100 - percent));
			progressGroup.down("div.download-background").setStyle({width:backgrndWidth+"px"});
		} else {
			Mojo.Log.error("Attachment ID ", id, " is invalid.");
		}
	},

	hideAttachmentList: function(event) {
		if (this.attachmentListShown) {
			var targetRow = this.controller.get(event.target);
			if (!targetRow.hasClassName('email-readview-attachments')) {
				targetRow = targetRow.up('div.email-readview-attachments');
			}

			// Only hide the attachments list if the mousedown target was outside the
			// list elements. Otherwise, reset the hide timer
			if (targetRow == null) {
				this.showHideAttachmentList();
			} else if (this.hideTimeout) {
				clearTimeout(this.hideTimeout);
				this.hideTimeout = setTimeout(this.showHideAttachmentList.bind(this), 15000);
			}
		}
	},

	showHideAttachmentList: function() {
		if (this.hideTimeout) {
			clearTimeout(this.hideTimeout);
			this.hideTimeout = null;
		}

		if (!this.attachmentListShown) {
			this.attachmentList.show();
			this.attachmentsShowElem.hide();
		}
		
        var options = {reverse:this.attachmentListShown,
		               onComplete: this.animationComplete.bind(this),
					   curve: 'over-easy',
					   from: 46,
					   to: this.attachmentListHeight,
					   duration: 0.6};
		Mojo.Animation.animateStyle(this.attachmentList, 'height', 'bezier', options);
	},

	animationComplete: function(listElem, cancelled) {
		if (!cancelled) {
			this.attachmentListShown = !this.attachmentListShown;
			if (this.attachmentListShown) {
				this.attachmentsShowElem.hide();
				this.attachmentList.show();

				this.hideTimeout = setTimeout(this.showHideAttachmentList.bind(this), 15000);
			} else {
				this.attachmentList.hide();
				this.attachmentsShowElem.show();
			}
		}
	},

	/**
	 * User clicked on a hyperlink.
	 */
	handleLinkClicked: function(event) {
		Mojo.Log.info("handleLinkClicked %s", event.url);
		this.controller.serviceRequest('palm://com.palm.applicationManager',
				{
					method: 'open',
					parameters: {target: event.url}
				});
	},

	/**
	 * WebView widget wants us to create a new page.
	 */
	handleCreatePage: function(event) {
		Mojo.Log.info("handleCreatePage: %s", event.pageIdentifier);
		this.controller.serviceRequest('palm://com.palm.applicationManager',
				{
					method: 'open',
					parameters: {
						'id': 'com.palm.app.browser',
						'params': {scene: 'page', pageIdentifier: event.pageIdentifier}
					}
				});
	},

	/**
	 * handle a menu command.
	 */
	handleCommand: function(event) {
		if (event.type == Mojo.Event.command) {
			try {
				switch (event.command) {
					case 'copyAll':
						this.copyAll(event);
						break;
						
					case 'reply':
						this.reply();
						break;

					case 'replyAll':
						this.replyAll();
						break;

					case 'forward':
						this.forward();
						break;

					case 'delete':
						this.deleteEmail();
						this.removeMessageFromParentScene();
						break;

					case 'move':
						this.controller.stageController.pushScene("moveto", this.account);
						this.removeMessageFromParentScene();
						break;

					case 'mark-unread':
						var currentLabel = this.markUnreadMenuItem.label;
						var markRead = (currentLabel == MessageAssistant.kAppMenuMarkRead);
						Message.setRead(this.data.id, markRead);
						if (markRead) {
							this.markUnreadMenuItem.label = MessageAssistant.kAppMenuMarkUnread;
						} else {
							this.markUnreadMenuItem.label = MessageAssistant.kAppMenuMarkRead;
						}
						
						this.updateMessageInParentScene();
						
						break;

					case 'flag':
						this.handleToggleFavorites();
						this.updateMessageInParentScene();
						break;
						
					case 'show-recipients':
						this.showHideRecipients();
						break;

					case Mojo.Menu.prefsCmd:
						MenuController.showPrefs(this.controller.stageController);
						break;

					case Mojo.Menu.helpCmd:
						MenuController.showHelp();
						break;
				}
			}
			catch (e) {
				Mojo.Log.error("MessageAssistant.handleCommand: "+ e.message +" in "+ e.sourceURL +"("+ e.line +")" );
			}
		}
		// Enable prefs & help menu items
		else if (event.type == Mojo.Event.commandEnable && 
		         (event.command == Mojo.Menu.prefsCmd || event.command == Mojo.Menu.helpCmd)) {
			event.stopPropagation();
		}
	}, 
	
	updateMessageInParentScene: function() {
		if (this.parentAssistant && this.itemIndex !== undefined) {
			this.parentAssistant.updateItemAtIndex(this.itemIndex);
		}
	},
	
	removeMessageFromParentScene: function() {
		if (this.parentAssistant && this.itemIndex !== undefined) {
			this.parentAssistant.removeItemAtIndex(this.itemIndex);
		}
	},
	
	handleInlineImageSaved: function(event) {
		if (event.status) { 
			var filepath = this.makeTitleFromUrl(event.filepath);
			var message = $L('Saving "#{path}"').interpolate({path: filepath});
			Mojo.Controller.appController.showBanner(
						{messageText: message},
						{banner: 'image', filename: event.filepath});
		}
	},
	
	makeTitleFromUrl: function(url) {
		if (url) {
			var result = url.match(/^.*\/([^\/]+)$/);
			if ((result !== null) && (result.length > 1)) {
				return result[1];
			}
		}
	
		return url;
	},
	
	handleWebViewSingleTap: function(event) {	
		try {
			var tapPt = Element.viewportOffset(this.webview);
			tapPt.left = event.centerX - tapPt.left;
			tapPt.top  = event.centerY - tapPt.top;

			//Mojo.Log.info("MessageAssistant.handleWebViewSingleTap(): event.altKey=%s, tapPt.left=%d, tapPt.top=%d", event.altKey, tapPt.left, tapPt.top);
			if (event.altKey) {
				var popupItems = [
					{label: $L('Open URL'), command:'openNew'},
					{label: $L('Share Link'), command:'shareUrl'},
					{label: $L('Copy URL'), command:'copyUrl'},
					{label: $L('Copy to Photos'), command:'copyToPhotos'},
					{label: $L('Share Image'), command:'shareImage'} //,
					// {label: $L('Set Wallpaper'), command:'setWallpaper'}
				];

				var findItem = function(command) {
					for (var i = 0, len = popupItems.length; i < len; i++) {
						if (popupItems[i].command === command) {
							return popupItems[i];
						}
					}
				};

				var selectedCommand;
				var imageInfo;
	
				var saveImageCallback = function(succeeded, path) {
					if (succeeded) {
						switch (selectedCommand) {
							case 'shareImage':
								this.shareImage(imageInfo, path);
								break;
							case 'setWallpaper':
								this.setWallpaper(path);
								break;
							case 'copyToPhotos':
								this.showOkAlert($L('Image Saved'),
									$L('The image was successfully added to your photo album.'));
								break;
						}
					}
					else {
						this.showOkAlert($L('Error Saving Image'),
								$L('There was an error saving the selected image.'));
					}
				}.bind(this);

				var urlInfo = {};
				var popupSelectFunc = function(value) {
					selectedCommand = value;
					
					switch (value) {
						case 'openNew':
							this.newBrowserPage(urlInfo.url);
							break;
						case 'shareUrl':
							this.shareUrl(urlInfo.url, urlInfo.desc);
							break;
						case 'copyUrl':
							this.controller.stageController.setClipboard(urlInfo.url);
							break;
						case 'copyToPhotos':
							this.webview.mojo.saveImageAtPoint(tapPt.left, tapPt.top, "/media/internal", saveImageCallback);
							break;
						case 'shareImage':
							this.webview.mojo.saveImageAtPoint(tapPt.left, tapPt.top, "/tmp", saveImageCallback);
							break;
						case 'setWallpaper':
							this.webview.mojo.saveImageAtPoint(tapPt.left, tapPt.top, "/media/internal", saveImageCallback);
							break;
					}
				}.bind(this);

				var imageInfoResponse = function(response) {
					imageInfo = response;
					var usedItems = [];
					
					if (urlInfo.url) {
						usedItems.push( findItem('openNew') );
						usedItems.push( findItem('shareUrl') );
						usedItems.push( findItem('copyUrl') );
					}
				
					if (response.src) {
						usedItems.push( findItem('shareImage') );
					}

					if (this.supportedImageType(response.src, response.mimeType)) {
						usedItems.push( findItem('copyToPhotos') );
						//usedItems.push( findItem('setWallpaper') );
					}
					
					if (usedItems.length) {
						this.controller.popupSubmenu({ onChoose: popupSelectFunc, items: usedItems });
					}
				}.bind(this);

				var urlInspectResponse = function(response) {
					urlInfo = response || {};
					this.webview.mojo.getImageInfoAtPoint(tapPt.left, tapPt.top, imageInfoResponse);
				}.bind(this);

				this.webview.mojo.inspectUrlAtPoint(tapPt.left, tapPt.top, urlInspectResponse);
			}
		}
		catch (e) {
			Mojo.Log.logException(e);
		}
	},
	
	supportedImageType:function(url, mimeType) {
		switch (this.getImageType(url, mimeType)) {
			case 'jpeg':
			case 'png':
			case 'bmp':	// We only support 24/32 bit BMP's and don't differentiate here
				// GIF not yet supported
				return true;
			default:
				return false;
		}
	},
	
	getImageType: function(url, mimeType) {
		url = url || '';
		mimeType = mimeType || '';
		var suffix = '';
		try {
			suffix = this.getResourceExtension(url);
			if (suffix === null) {
				suffix = '';
			}
		}
		catch (e) {
			Mojo.Log.logException(e);
		}

		suffix = suffix.toLowerCase();
		mimeType = mimeType.toLowerCase();

		if (suffix === 'jpg' || suffix === 'jpeg' || suffix === 'jpe' || mimeType === 'image/jpeg') {
			return 'jpeg';
		}
		else if (suffix === 'bmp' || mimeType === 'image/bmp') {
			return 'bmp';
		}
		else if (suffix === 'png' || mimeType === 'image/png') {
			return 'png';
		}
		else if (suffix === 'gif' || mimeType === 'image/gif') {
			return 'gif';
		}
		else {
			return 'unknown';
		}
	},
	
	getResourceExtension: function(url) {
		var p = new Poly9.URLParser(url);

		var matches = p.getPathname().match(/\.([^\.]*)$/i);
		if (matches) {
			return matches[1];
		}
		else {
			return null;
		}
	},
	
	newBrowserPage: function(url) {
		this.controller.serviceRequest('palm://com.palm.applicationManager',
			{
				method: 'open',
				parameters: {target: url}
			});
	},
	
	shareUrl: function(url, title) {
		if (url === undefined) {
			return;
		}

		if (!title) {
			try {
				title = $L("page at #{host}").interpolate({host: UrlUtil.getUrlHost(url)});
			}
			catch (e) {
				title = url;
			}
		}

		var text = $L("Here's a website I think you'll like: <a href='#{src}'>#{title}</a>").interpolate(
				{src: url, title: title});
		var parameters = {
			summary: $L('Check out this web page...'),
			text: text
		};
		var email = new Email();
		email.evalParams(parameters);
		AppAssistant.openComposeStage(email);
	},
	
	shareImage: function(imageInfoObj, pathToImage) {
		var title;
		if (imageInfoObj.title && imageInfoObj.title.length) {
			title = imageInfoObj.title;
		}
		else if (imageInfoObj.altText && imageInfoObj.altText.length) {
			title = imageInfoObj.altText;
		}
		else {
			var p = new Poly9.URLParser(imageInfoObj.src);
			
			title = $L('picture link');
			try {
				if (imageInfoObj.src !== 'data:') {
					title = $L("picture at #{host}").interpolate(
							{host: p.getHost()});
				}
			}
			catch (e) {}
		}

		var text = $L("Here's a picture I think you'll like: <a href='#{src}'>#{title}</a>").interpolate(
				{src: imageInfoObj.src, title: title});

		var parameters = {
			summary: $L('Check out this picture...'),
			text: text,
			attachments: [{fullPath: pathToImage}]
		};
		var email = new Email();
		email.evalParams(parameters);
		AppAssistant.openComposeStage(email);
	},
	
	setWallpaper: function(pathToImage) {
		var errorTitle = $L("Error Setting Wallpaper");
	
		var onSetSuccess = function(response) {
			this.showOkAlert($L("Wallpaper has been set"),
					$L("The image has been successfully set as your wallpaper."));
		}.bind(this);
	
		var onSetFailure = function(response) {
			this.showOkAlert(errorTitle, $L("Cannot set picture as current wallpaper."));
		}.bind(this);

		var onImportSuccess = function(response) {
			this.controller.serviceRequest('palm://com.palm.systemservice/',
			{
				method : 'setPreferences',
				parameters: {wallpaper: response.wallpaper},
				onSuccess: onSetSuccess,
				onFailure: onSetFailure
			});
		}.bind(this);
	
		var onImportFailure = function() {
			this.showOkAlert(errorTitle, $L("Cannot import picture into wallpaper database."));
		}.bind(this);

		this.controller.serviceRequest('palm://com.palm.systemservice/', {
				method : 'wallpaper/importWallpaper',
				parameters: {
					target: encodeURIComponent(pathToImage),
					scale: "1.0"
				},
				onSuccess: onImportSuccess,
				onFailure: onImportFailure
			});
	},
	
	showOkAlert: function(title, message)
	{
		this.controller.showAlertDialog({
			title: title, message: message,
			choices:[{label:$L('OK'), value:'1', type:'dismiss'}]
		});
	},

	handleNextMessagesResponse: function(response) {
		this.nextMessages = response;
		
		var prevEmail = this.controller.get('previous_email');
		if (!prevEmail) {
			Mojo.Log.error("previous_email element does not yet exist");
		} else {
			if (response.newer === undefined || response.newer.end) {
				// hide 'previous_email' because there's no more emails in that direction
				prevEmail.addClassName('disabled');
			} else {
				prevEmail.removeClassName('disabled');
			}
		}

		var nextEmail = this.controller.get('next_email');
		if (!nextEmail) {
			Mojo.Log.error("next_email element does not yet exist");
		} else {
			if (response.older === undefined || response.older.end) {
				// hide 'next_email' because there's no more emails in that direction
				nextEmail.addClassName('disabled');
			} else {
				nextEmail.removeClassName('disabled');
			}
		}
	},

	gotoNextEmail: function(direction) {
		if (this.nextMessages !== undefined && this.nextMessages[direction] !== undefined) {
			var details = this.nextMessages[direction];
			if (!details.end && details.id > 0) {
				if (this.transition !== null) {
					this.transition.cleanup();
				}
				this.transition = this.controller.prepareTransition(Mojo.Transition.crossFade, false);
				this.prepareForNewMessage(details);
			}
		}
	},

	/* prep screen for next/previous message selection */
	prepareForNewMessage: function(details) {
		this.data = { id: details.id, senderDetails:{} }; // data.id
		this.account = {};
		this.gotFirstResponse = false;
		this.bodyLeftOffset = 0;
		this.prerendered = false;
		this.attachmentsRendered = false;
		this.renderedMessageBody = false;
		this.emailLoaded = false;
		this.pendingComposeActions.clear();
		// Clear out the old email body by loading a simple empty page
		this.webview.mojo.openURL(Mojo.appPath + "emptypage.html");
		this.controller.get('email-readview-attachments-block').hide();

		// stop listening to all these
		this.controller.get('previous_email').stopObserving(Mojo.Event.tap, this.boundGotoNextEmailNewer);
		this.controller.get('next_email').stopObserving(Mojo.Event.tap, this.boundGotoNextEmailOlder);
		if (this.recipientController) {
			this.recipientController.up(".palm-row").stopObserving(Mojo.Event.tap, this.boundShowHideRecipients, true);
		}
		var elem;
		elem = this.controller.get('invite-accept');
		if (elem) {
			elem.stopObserving(Mojo.Event.tap, this.boundHandleInviteResponseAccept);
		}
		elem = this.controller.get('invite-tentative');
		if (elem) {
			elem.stopObserving(Mojo.Event.tap, this.boundHandleInviteResponseTentative);
		}
		this.controller.get('invite-decline');
		if (elem) {
			elem.stopObserving(Mojo.Event.tap, this.boundHandleInviteResponseDecline);
		}
		
		this.toggleHighlightHeaderContent(false);

		// reset the horizontal positioning of these two blocks.
		this.emailHeaderBlock.setStyle({ 'left': '0px' });
		this.emailPicturesBlock.setStyle({ 'left': '0px' });

		// Subscribe to the new message
		if (this.waitingForMessageBodyTimeout !== undefined) {
			clearTimeout(this.waitingForMessageBodyTimeout);
			this.waitingForMessageBodyTimeout = undefined;
		} else {
			if (this.messageTimedOut) {
				this.messageTimedOut = undefined;
				this.hideDownloadErrorUI();
				this.controller.get('retry-message-download').stopObserving(Mojo.Event.tap, this.boundHandleMessageDownloadRetry);
			}
		}
		if (this.messageSubscription) {
			this.messageSubscription.cancel();
		}
		this.enableMessageSubscription();
	},

	/**
	 * Called by the webview widget when setup is complete?
	 */
	ready: function() {
	    this.webview.mojo.addUrlRedirect("^file:.*", false, "", 0);
	    this.webview.mojo.addUrlRedirect(".*", true, "", 0);
		this.webview.mojo.setEnableJavaScript(false);
	},

	setupMessage: function() {
		// subscribe to the message details because the transport may need to send updates in the case where the
		// email body and/or attachments isn't yet downloaded
		this.enableMessageSubscription();
		
		// Setup the WebView for the body text
		// TODO autowidth to true?
		var emailStageController = Mojo.Controller.appController.getStageController("email");
	    var attr = {minFontSize:18,
					cacheAdapter:true,
					fitWidth: false,
					virtualpagewidth: emailStageController.window.innerWidth,
					minimumpageheight: 32, // Start out very short in case the message body empty
					showClickedLink:true};
	    this.controller.setupWidget('email_body_text', attr);
		this.controller.setupWidget('email_no_body_spinner', { spinnerSize: Mojo.Widget.spinnerSmall });
		
		this.webview = this.controller.get('email_body_text');
		
		// setup widgets
		this.webview.addEventListener(Mojo.Event.webViewUrlRedirect, this.boundHandleLinkClicked, false);
		this.webview.addEventListener(Mojo.Event.webViewMimeNotSupported, this.boundHandleLinkClicked, false);
		this.webview.addEventListener(Mojo.Event.webViewMimeHandoff, this.boundHandleLinkClicked, false);
		this.webview.addEventListener(Mojo.Event.webViewImageSaved, this.boundHandleInlineImageSaved, false);
		this.webview.addEventListener('singletap', this.boundHandleWebViewSingleTap, true);

		this.waitingForMessageBody = undefined;
	},
	
	enableMessageSubscription: function() {
		this.messageSubscription = new Mojo.Service.Request(Message.identifier, {
				method: 'messageDetail',
				parameters: { 'message':this.data.id, 'folderFilter': this.folderFilter, 'folder': this.folderId, subscribe:true },
				onSuccess: this.messageDetailsUpdated.bind(this),
				onFailure: this.waitMessageError.bind(this)
			});
	},

	orientationChanged: function(orientation) {
		if (orientation === "left" || orientation === "right") {
			this.controller.sceneElement.addClassName('landscape');
		} else {
			this.controller.sceneElement.removeClassName('landscape');
		}
	},

	focusEmailStage: function() {
		if (this.focusStageTimer) {
			this.focusStageTimer = undefined;
			AppAssistant.focusEmailStage();
		}			
	},

	// This is called from accountpreference assistant when the user removes the account
	accountDeletedNotification: function(accountId) {
		if (accountId === this.account.account) {
			Mojo.Log.warn("MessageAssistant is showing a deleted account, setting up for cleanup");
			this.popOnActivate = true;
		}
	},
	
	copyAll: function(event) {
		this.toggleHighlightHeaderContent(true);
		
		// Get the content from the body.
		this.webview.mojo.selectAll();
		this.webview.mojo.copy(this.copiedHandler.bind(this));
	},
	
	copiedHandler: function() {
		this.tempTextArea = document.createElement('textarea');
		this.tempTextArea.setAttribute('class','text-hidden');
		this.controller.stageController.activeScene().sceneElement.appendChild(this.tempTextArea);
		this.tempTextArea.select();
		PalmSystem.paste();
		
		if (this.pasteTimeout)
			clearInterval(this.pasteTimeout);
		this.pasteTimeout = setTimeout(this.getBodyContent.bind(this), 1);
	},
	
	getBodyContent: function(){
		clearInterval(this.pasteTimeout);
		this.pasteTimeout = undefined;
		this.bodyContent = this.tempTextArea.value;
		this.tempTextArea.remove();
		this.copyEmailContentToClipboard();
	},
	
	copyEmailContentToClipboard: function() {
		var tempTextArea = document.createElement('textarea');
		tempTextArea.value = this.getEmailHeaderContent() + "\n\n" + this.bodyContent;
		this.controller.stageController.activeScene().sceneElement.appendChild(tempTextArea);
		tempTextArea.select();
		this.controller.document.execCommand('copy');
		tempTextArea.remove();
		this.bodyContent = undefined;
	},
	
	getEmailHeaderContent: function() {
		var recipientList = EmailRecipient.separateRecipients(this.data.recipients);
		var theDate = new Date(parseInt(this.data.timeStamp, 10));
		var formattedDate = Mojo.Format.formatDate(theDate, {date:'medium', time:'short'});
		
		// construct email header string that will be combined with email body.
		var headerInfo = "From: " + recipientList[EmailRecipient.roleFrom][0].displayName;
		headerInfo += "\nDate: " + formattedDate;
		
		if (recipientList[EmailRecipient.roleTo].length > 0)  {
			headerInfo += "\nTo: "; 
			var recipArr = recipientList[EmailRecipient.roleTo];
			for (var i = 0, len = recipArr.length; i < len; i++) {
				if (i > 0)
					headerInfo += "; ";
				headerInfo += recipArr[i].displayName;
			} 
		}
		
		if (recipientList[EmailRecipient.roleCc].length > 0) {
			headerInfo += "\nCC: "; 
			var ccArr = recipientList[EmailRecipient.roleCc];
			for (var i = 0, len = ccArr.length; i < len; i++) {
				if (i > 0)
					headerInfo += "; ";
				headerInfo += ccArr[i].displayName;
			} 
		}
		
		if (this.data.summary)
			headerInfo += "\nSubject: " + this.data.summary;
		
		return headerInfo;
	},
	
	toggleHighlightHeaderContent: function(on) {
		var headerElements = [];
		headerElements.push('email_from');
		headerElements.push('email_recipients_controller');
		headerElements.push('email_recipients');
		headerElements.push('email_subject');
				
		headerElements.each(function(elemName) {
			var elem = this.controller.get(elemName);
			if (on) 
				elem.addClassName('copy-all-highlighted');
			else
				elem.removeClassName('copy-all-highlighted');
		}.bind(this));
	},
	
	handleDOMClicked: function(event) {
		this.toggleHighlightHeaderContent(false);
	},

	reply: function() {
		if (this.emailLoaded) {
			this.doReply();
		}
		else {
			this.pendingComposeActions.push(this.doReply.bind(this));
		}
	},
	
	doReply: function() {
		var email = new Email();
		email.createReply(this.data, this.account.login);
		MenuController.showComposeView(email, undefined, this.controller, this.cmdMenuModel);
	},

	replyAll: function() {
		if (this.emailLoaded) {
			this.doReplyAll();
		} else {
			this.pendingComposeActions.push(this.doReplyAll.bind(this));
		}
		
	},
	
	doReplyAll: function() {
		var email = new Email();
		email.createReplyAll(this.data, this.account.login);
		MenuController.showComposeView(email, undefined, this.controller, this.cmdMenuModel);
	},

	forward: function() {
		if (this.emailLoaded) {
			this.doForward();
		} else {
			this.pendingComposeActions.push(this.doForward.bind(this));
		}
	},
	
	doForward: function() {
		var email = new Email();
		email.createForward(this.data, this.account.login);
		MenuController.showComposeView(email, undefined, this.controller, this.cmdMenuModel);
	},
	
	handlePendingComposeActions: function() {
		Mojo.Log.info("Email is fully loaded, so start to execute pending compose actions");
		this.pendingComposeActions.each(function(func) {
			if (func) {
				func.apply();
			}
		});
	},

	deleteEmail: function() {
		Message.setDeleted(this.data.id, true);
		this.controller.stageController.popScene();
	}
});

MessageAssistant.kAppMenuMarkRead = $L('Mark as Read');
MessageAssistant.kAppMenuMarkUnread = $L('Mark as Unread');
MessageAssistant.kAppMenuSetFlag = $L('Set Flag');
MessageAssistant.kAppMenuClearFlag = $L('Remove Flag');
MessageAssistant.kAppMenuShowRecipients = $L('Show Recipients');
MessageAssistant.kAppMenuHideRecipients = $L('Hide Recipients');
MessageAssistant.kDummyResponse = {"match": null, "record": {"id": 0}}; // used for lookup misses
MessageAssistant.recipientCache = new ObjCache(64);
