Monthly Archives: March 2014

Adventures in URL Hacking ‘Send an Email’ on Cases

URL hacking the SFDC Send an Email page is well-known. See for example:

Adventure starts
In one of my orgs, we were using On Demand Email-to-Case. If you’ve used this, you know that SFDC creates a thread_id value based on the orgId and case Id wrapped in special keywords (e.g. ref:_00DK0AaiLo._500K05CUEs:ref) . Any subsequent email sent from the Case will include this thread_id in the body and subject line so that if there is an email reply to your On Demand email-to-case email address, the reply is attached to the originating case (and doesn’t create a new case).

I had a custom VF page associated to the Case object and had added a custom button to send an email. Here was the URL hack (line breaks added for clarity):

<apex:commandButton 
  action="{!URLFOR($Action.Activity.SendEmail,csW.cs.id,
  [p2_lkid=csW.cs.contactId,
  rtype=LPAD('3',3,'00'),
  p3_lkid=csW.cs.id,
  retURL=$CurrentPage.url])}"
 value="Send an Email"/>
  • csW.cs.id refers to a controller variable csW of type CaseWrapper that had a member variable cs of type Case.
  • Per the URL Hack tips, p3_lkid refers to the ID of the target object used for merge fields – here, a Case object. p2_lkid references the recipient of the email – here, a Contact record id.

Adventure becomes a mystery
Given the following sequence:

  1. Customer sends email to Foo-support@ourdomain.com
  2. Customer receives auto-reply email containing the thread_id
  3. From the Case, Support team replies to the original email message (from: foo-support@ourdomain.com)
  4. Customer replies to message in step 3; this reply is properly attached to the Case.
  5. From the Case, Support team sends, using the custom button, a new email messsage to the customer (from: foo-support@ourdomain.com).
  6. Customer replies to message in step 5. This reply creates a brand new case!

It was as if emails generated from the ‘Email Message’ reply link had the proper thread_id but emails generated from the ‘Send and Email’ url hack did not – yet both resolved to the proper SFDC page with all fields filled in correctly.

Adventure Ends
This took me a while to figure out what with the flurry of emails to inspect and that the SFDC page that sends the emails looked just fine.

Here was the solution:

In my URL hack for ‘send an email’, when merge fields are resolved by SFDC, I got this:

https://cs9.salesforce.com/_ui/core/email/author/EmailAuthor
?p2_lkid=003K000000jbawQIAQ
&rtype=003
&p3_lkid=500K0000005CUEsIAO
&retURL=...

Note that &p3_lkid is an 18 character ID of the Case. When the email message was sent, SFDC used this 18 character ID and generated a threadId of ref:_00DK0AaiLo._500K05CUEsIAO:ref.

Contrast this thread_id with the thread_id generated by SFDC when the Case was originally created: ref:_00DK0AaiLo._500K05CUEs:ref, That is:

ref:_00DK0AaiLo._500K05CUEsIAO:ref
ref:_00DK0AaiLo._500K05CUEs:ref

Even though SFDC normally treats 18 character IDs the same as 15 character IDs, if the value of the URL hack field p3_lkid=csW.cs.id is an 18 character ID, the Send an Email SFDC code (not mine) creates a thread_id that is 3 characters longer than the thread_id normally generated when the Case was created via On Demand Email-to-Case

Obviously, the problem is that the ‘Send an Email’ code expected the value of &p3_lkid=500K0000005CUEsIAO to be a 15 character ID, not an 18 character Id, that is: &p3_lkid=500K0000005CUEs.
In other words, the SFDC Send an Email code is not independent of p3_lkId length (15 versus 18). Compounding the mystery was that Email To Case parses thread_ids in such a way as to ignore the 15 vs 18 character ID origin.

Thus, the solution was to change the URL Hack to the following (by substringing the 18 character Case Id to a 15 character value):

<apex:commandButton 
  action="{!URLFOR($Action.Activity.SendEmail,csW.cs.id,
  [p2_lkid=csW.cs.contactId,
  rtype=LPAD('3',3,'00'),
  p3_lkid=LEFT(csW.cs.id,15),
  retURL=$CurrentPage.url])}"
 value="Send an Email"/>

Soft alerts

In several projects I have done, the users don’t want to be bothered with required fields or validation errors on Accounts or Opportunities until they get closer to quotation or order fulfillment. Yet marketing and management reports frequently want ‘categorization’ or ‘taxonomic’ fields filled in (but can live with them blank).

To resolve this in a proactive way and still use SFDC out-of-box page layouts – no VF custom controller or controller extension , I use soft alerts on Account and Opportunity that warn the user that data is not complete and that it should be completed –but it is not urgent that it is done so right this minute.

Here is an example Foo__c object with a soft alert when the Name field contains a ‘colorful’ word
Foo_fubar

This is done with a formula field that leverages the IMAGE() function as shown here:

/* Form of an alert is a set of concatenated strings as follows:

IF(<condition 1>,
IMAGE("/img/samples/flag_yellow|red|green.gif" , 'Yellow|Red|Green' ,12,12) & ' alert text with leading space' & BR() ,
NULL
) &
next condition as above for second alert */


IF (CONTAINS(Name, 'fubar') ,
IMAGE("/img/samples/flag_red.gif" , 'Red' ,12,12) & ' Name contains a colorful slang word' & BR(),
NULL)
&
IF (CONTAINS(Name, 'sylvan') ,
IMAGE("/img/samples/flag_green.gif" , 'Green' ,12,12) & ' Name contains a peaceful word' & BR(),
NULL)

The solution has some limitations as the total size of the formula must be under 5000 characters but for a dozen or so possible alerts, you can avoid using a VF controller. Note the second alert that appears if the Foo__c.name field has a nice word as shown here:
foo_sylvan

What if there are no alerts – how to suppress the custom field on the standard page layout?
If there are no alerts, why waste the valuable line to show nothing? Unfortunately, SFDC standard page layouts don’t permit conditional rendering of fields. Now, you need to escape to Visualforce but the goal is to preserve the advantages of your page layout using apex:detail.

We’ll start with a simple VF page (and we assume that inlineEdit is enabled for your users as it is so convenient):

<apex:page standardcontroller="Foo__c" >
	<apex:pageMessages id="pageMsgs" showDetail="true"/>
	<apex:detail inlineEdit="true" relatedList="true" showChatter="false" subject="{!Foo__c.id}" />	

</apex:page>

In order to suppress the Alerts field, we’ll need to use jQuery to locate the DOM elements and remove them when the value is null. So, here is the expanded page:

<apex:page standardcontroller="Foo__c" >

<apex:includescript value="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js" />

<script>
		$j = jQuery.noConflict();		//	reassign $ to $j to avoid conflict with VF
		var	fooViewJQApp	= {};       //	define the application
		$j(function() {					// 	jQuery shorthand for $(document).ready(function() ..  Wait until page loads 

			(function(app) {			//	Define anon function
				//	--------------------------
				//	variable definitions
				//	--------------------------
				
				//	--------------------------
				//	init:	do initializations
				//	--------------------------
				app.init	= function() {
					app.bindings();		// establish bindings
					app.hideNullAlerts();  // hide the alerts field on page load if null
				};				
				//	-----------------------------------
				//	bindings:  events to event handlers
				//	-----------------------------------
				app.bindings = function() {
					// set up binding for events
                };	// end all bindings				
				
				//	------------------------------------
				//	hideNullAlerts:	Alerts formula field takes up a row and should be suppressed if null (Caption + field)
				//	------------------------------------
				app.hideNullAlerts = function() {
					$j('.labelCol').each(function() {
						if ($j(this).text() == 'Alerts') {		// found the <td> for the caption
							var $alertVal = $j(this).next();	// get adjacent td for the inlineEdit value
							if ($alertVal.text().length == 0) {	// formula field if no value has length 0
								$j(this).parent().remove();			// locate the parent of the alerts label (a tr) remove the <tr> and everything inside it
								return false;						// breaks out of each loop	as we found the alerts field
							}	
						}
					});
				};

				app.init();			// Anon function invokes its own init function
			}) (fooViewJQApp);		// Execute anon function passing in an object representing our application
		});			
	</script>


	<apex:pageMessages id="pageMsgs" showDetail="true"/>
	<apex:detail inlineEdit="true" relatedList="true" showChatter="false" subject="{!Foo__c.id}"  
                     oncomplete="fooViewJQApp.hideNullAlerts();"/>	

</apex:page>

Now, this deserves some explanation:

  1. The jQuery pattern I borrowed from the kittyDressUp pattern in “jQuery Mobile Web Development Essentials” Chapter 7. It provides a nice place to hang all your functions and provide some order and consistency to using jQuery and javascript on your VF page. When you inspect a VF page in Eclipse, it is jarring to see VF markup and javascript interspersed and it can make the page harder to read as there are two different technologies at play working on a common DOM model. I didn’t need to set up any bindings but I left in the placeholder for other future jQuery uses.
  2. When the page loads, the jQuery app’s hideNullAlerts() function is invoked. When the page partial refreshes on an inlineEdit save, the apex:detail oncomplete event is invoked by VF and that too, calls hideNullAlerts().
  3. Using standard page layouts, there is no obvious id field to look for to find the DOM element corresponding to the ‘Alerts’ caption and alerts content field. You could use the SFDC-generated ID but I hate hardcoding ids in anything. Firebug or its equivalent is your friend here to understand the SFDC VF DOM structure for a standard page layout (see next figure). The $j('.labelCol') locates all elements on the page which are captions. Then, each() is used to go through every caption looking for the Alerts caption. Upon locating it (a td tag), the function then looks for the adjacent next sibling (also a td) and tests to see if the result of text() is of length 0. Note that jQuery text() returns the combined contents of the selected element and all descendents. If so, then the alert is null. So, go up one level to the tr and remove it. Since there is only one alert field to remove, stop the each() loop by returning false.

  4. Here is an excerpt of the HTML generated by VF for a standard pagelayout that has inlineEdit enabled. As always, when messing about with the SFDC-generated HTML, you are subject to changes in future releases.

    <table class="detailList" cellspacing="0" cellpadding="0" border="0">
    <tbody>
    <tr>
      <td class="labelCol">Alerts</td>
      <td id="00NK0000001OD2cj_id0_j_id28_ilecell" class="data2Col inlineEditLock" onmouseover="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.mouseOverField(event, this);" onmouseout="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.mouseOutField(event, this);" onfocus="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.mouseOverField(event, this);" ondblclick="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.dblClickField(event, this);" onclick="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.clickField(event, this);" onblur="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.mouseOutField(event, this);" colspan="3">
        <div id="00NK0000001OD2cj_id0_j_id28_ileinner">
         <img width="12" border="0" height="12" alt="Green" src="/img/samples/flag_green.gif">
    Name contains a peaceful word
        <br>
        </div>
      </td>
    </tr>
    <tr>
    <tr>
    <tr>
    </tbody>
    </table>