Skip to content
May 23 10

Email Handler for Chatter

by Evan Callahan

I wrote an application for the Chatter Developer Challenge, which you can install free from the AppExchange.  It is called Email2Chatter, and it is an inbound email handler that automatically posts the subject and body text of email you send to your Chatter feed.

Email2Chatter supports email attachments, posting them to the Chatter feed for download, and also supports forwarded emails, which it attaches to the feed post as HTML or text.

Email2Chatter is available free of charge with complete source code under GPL license. Once you install the AppExchange package, you’ll need to configure the inbound email handler before you can send it email.

First, go to Setup => Develop => Email Services.  Create an email service:

  • Name: Chatter
  • Apex Class: ChatterEmail
  • Accept Attachments: All
  • Accept Email From: [your organization’s domain name]
  • Truncate Oversize: [checked]
  • Convert Text Attachments: [checked]
  • Active: [checked]

Next, click New Email Address:

  • Email Address: chatter
  • Active: [checked]
  • Context User: [name of system administrator]
  • Accept Email From: [your organization’s domain name]

Make note of the email address – that is where users can send or forward email and attachments to post to their Chatter feed.

May 18 10

Geocoding and Districting

by Evan Callahan

Groundwire’s latest project on Code Share is Geocoding and Districting. It uses address information to look up the latitude, longitude, congressional district, and state legislative districts for each contact and account entered into Salesforce.

I got the idea for this when I discovered Mobile Commons‘ free web service for looking up district information. Because I know that many nonprofits – including a number of Groundwire’s clients – pay significant fees to obtain this information, I was motivated to find out if I could retrieve it automatically.

The code was interesting to write for a few reasons:

  • There are two steps. First, you have to combine the full address and us it to look up the latitude and longitude. I used a web service called geocoder.us, which has both free and premium versions (free is limited to one lookup every 15 seconds). Then you send the latitude and longitude to the Mobile Commons web service to get the districts. I created a separate Apex class for each of these integrations.
  • I took advantage of the new built-in DOM support for XML district data, which is said to be faster and simpler than the older XMLDom class. (I actually found the new support to be sadly lacking in many ways, but I later found this nifty class which fills in the gaps.)
  • Because external callouts can’t be called directly from a trigger, I had to use the Apex @future notation to call the web services asynchronously. The coding happens a few seconds after you save a record, so you have to refresh the browser to see the result.
  • Calling out to a web service offers plenty of opportunities for error. In order to make my classes easier to use, I masked the complexity by handling all errors the same way, whether they were errors reported by the webservice, errors encountered during the callout process, or Apex exceptions. In every case, I simply set a string property called Error and failed gracefully; this makes the class much easier to use. In my trigger, I save any errors I receive in fields on Contact or Account, so you can see whether the geocoding and districting were successfully (or why not).
// example of how to find the district from the lat/lng
GW_MobileCommons mc = new GW_MobileCommons();
mc.latitude = 48;
mc.longitude = -122;
mc.getDistricts();
if (mc.error == null) {
   system.debug('Federal Congressional: ' + mc.congress);
   system.debug('State Senate: ' + mc.stateSenate);
   system.debug('State House: ' + mc.stateHouse);
} else {
   system.debug('ERROR: ' + mc.error);
}

If you visit the Code Share page to grab the source code, you’ll also find an AppExchange package link that allows you to install these tools directly into your own Salesforce instance. Please note the configuration steps: you have to add the two web service URLs to your Network Sites list before this will work. Also note that there are custom settings you can modify, which let you turn off the trigger for one or more actions.

Apex limits your web callouts to 10 at a time (and the free geocoding service limits them to just one), so this trigger isn’t going to work during a bulk data import or update. But when entering one record at a time, this is a great way to ensure that you have the geocoding and district information right away – and without any charge for the service.

May 12 10

Code Sharing

by Evan Callahan

If you follow the new projects on Force.com Code Share, you may have noticed that Groundwire has posted several open-source projects there. Since Salesforce created the Code Share site a year and a half ago, I’ve benefited from a lot of great sample code posted there.  I had always hoped to participate in projects or share code of my own.

My colleagues and I have recently been able to find the time to post a few projects, and we hope to share more over time. (See my post on the Groundwire blog about why we like to share our code.)  I have already learned a lot in the process of putting these together, and it is getting easier each time.

One thing that I notice about Code Share, however, is that the promise of open-source collaboration doesn’t really seem to be taking hold. Very few projects have more than one contributor, and none have caught on as a place for community collaboration.  If you have ideas or code to contribute to one of Groundwire’s projects, please let us know and we’ll grant you access; or, if you have a project that would benefit from collaboration, especially one that would be helpful to nonprofits, let us know and we’ll see if we can help.

Apr 5 10

Combine campaigns and exclude members with our new Campaign Combiner

by Dave Manelski

Campaigns in Salesforce offer many powerful features, and we encourage our nonprofit clients to use them for all sorts of things:  email outreach lists, mailings, event participants, lists of people taking part in activism, and so on.  We love the way that Campaigns allow you to add people from multiple lists or reports without ever adding the same contact more than once.

As users become more sophisticated in their use of Campaigns, they inevitably ask us for something that Campaigns can’t do: exclude or remove a specific set of campaign members while adding others.  For example, suppose you send out a series of three emails to a large set of contacts by adding them to three different campaigns.  Now you want to send something to the same list as the first and second email, except that you want to exclude the people who were part of the third campaign – this new message doesn’t apply to them.  Salesforce has no way of helping you do this; you would have to manually remove the members from the campaign one by one.  (Groundwire is not alone in noticing the need for this capability, by the way.  Search for “campaign remove” on the Salesforce IdeaExchange and you’ll find  three different versions of this same request.)

To meet this need, my colleague Evan Callahan put together a flexible Visualforce interface called the Campaign Combiner. It allows you to add campaign members from multiple campaigns into a target campaign, or simultaneously exclude members who belong to multiple campaigns. Here is a video that shows it in action:

We think this is a really useful tool, so we’ve made it freely available from the AppExchange.  It is a “private” listing – to find it, go to the Campaign Combiner page on Code Share, and then click Install.  If you are an Apex developer, you may want to review the source code you’ll find there as well.

At Groundwire, we are on a mission to get affordable and innovative online tools and strategies into the hands of organizations building a sustainable world. Read more about our plans at http://groundwire.org/labs, and keep an eye out on the Force.com Code Share site for other nonprofit-friendly tools in the near future.

Dec 10 09

Groundwire is hiring a CRM Consultant

by Dave Manelski

I’m excited to report that our veteran CRM team at Groundwire is growing.  We looking for a new CRM Consultant.  This is a full-time, Seattle-based position.

I can’t say enough about my organization — our staff is made up of people who are creative, knowledgeable and passionate about the work we do.  We are capacity-builders, collaborators, and pioneers in our field. And we have a lot of fun:

At Groundwire we believe that relationships are the building blocks for all social change, and that technology is the “secret sauce” for supporting deeper relationships between organizations, activists, policy makers, and the overwhelming majority of us who care about the environment.

The Groundwire vision was conceived in 1995 by two people who imagined an organization that would deliver the environmental community from the technological dark ages. They dreamed of a tech-savvy team that could equip advocates and activists with the tools and skills to help them save our planet. For the past 14 years, we have been doing just that – from the days of setting up local servers and email accounts to our focus today on state-of-the-art online communications and constituent relationship management systems.

We need an experienced CRM Consultant to build customized databases that transform the effectiveness of the environmental movement.

Interested? Learn more or apply online.

Oct 29 09

Salesforce data storage woes

by Dave Manelski

Data storage is what is on my mind these days.  Like a gathering storm on the horizon, storage is a finite resource that is fast being consumed by my non-profit clients. The more successful the clients in their outreach efforts, the faster they are running out of space.

The number one culprit is popular blast email platform VerticalResponse (VR). Out of the box, VR creates a 2KB record associated with every contact or lead on a campaign used to generate an email list.  For organizations with relatively small lists, using campaigns & VR works great, it’s a fantastic platform integration.  For organizations with larger lists however, those 2KB records really add up fast.  Most non-profit organizations are under 50 user accounts and are allotted the minimum amount of data storage – 1GB – which isn’t a great deal of storage space no matter how you slice it.

Compounding the problem, on October 20th of this year, Salesforce began assessing 1KB per each new Campaign Member record. Up until now, Campaign Member records did not impact your data storage. Thought of as a second-class object, Campaign Member lived in a separate table in one of the older and cobweb infested corners of the database. If you wanted to keep track of a person’s meal preferences for an event, or manage an automated email drip campaign in Salesforce, you were out of luck. It was impossible to add custom fields to Campaign Member, fire an apex trigger or workflow rules. With the Summer ’09 Release however, came a slew of exciting new improvements to Campaign Member, now a first-class object in the database complete with custom fields, apex triggers, and workflow rules.   And with the Winter ’10 Release, Salesforce introduced even more improvements like roll-up summary fields to Campaign and Campaign Member record types. These new features come with a cost — it takes storage space for custom fields and additional processor cycles for workflow and triggers — I get why, I really do.

But here’s the rub…

Screen shot 2009-10-29 at 9.06.55 AMOver the course of time, Campaign Member records are always being created.  Growth is linear and the growth pattern is far steeper than that of contacts or accounts.  The size of your constituency, while (ideally) growing, is going to be correlated directly with the size of your organization’s user base.  Chances are if the number of contacts in your database increases from 10,000 to 100,000, so too is the size of your staff (user base) and the allotted data storage to house all of these new records.  And the opposite holds true, if your organization is not growing, data storage needed for the contacts and accounts in your database remains constant.

I work with environmental organizations, whose primary focus is engaging people in building a more sustainable society.  These organizations leverage tools like Salesforce and VerticalResponse to reach a large audience on very lean budgets and with relatively few staff.  As a consultant, I teach these organizations to use campaigns to track their touches with constituents over time, preserving the rich history of engagement.  With this ever-increasing mountain of campaign data, they can take advantage of Salesforce’s powerful reporting features to better target communications, reach out more effectively, and keep growing their lists. The more successful the organization in these efforts, the faster they run out of data storage, but their success still precludes most of my clients from being able to afford purchasing additional storage at $1,500 annually per 500MB chunk.

It’s a data storage nightmare.  Deleting or archiving campaigns isn’t really an option as that really defeats the purpose of having a high-functioning CRM like Salesforce.

The only real solution is steadily increasing the data storage allotment over time. I know that massive Oracle clusters aren’t cheap, but Salesforce should be adding servers and increasing the data storage allotment for each instance, each year. Now that they’re charging for Campaign Member records, it’s only a matter of time before more customers start feeling the pinch.  In the meantime, you can do your part by voting up this idea on the IdeaExchange.

Oct 21 09

Salesforce Sites page as a form endpoint

by Dave Manelski

I’m working on a project right now that requires collecting contact information through a number of web forms into Salesforce.  The standard web-to-lead form simply won’t work for this particular client, because they’re anticipating a big media pop from their outreach campaign, which would easily overwhelm the 500 sign-ups per day limit. A Salesforce Sites form seemed to hold the solution, but here’s where it gets tricky.  The client’s website is highly stylized, which makes embedding a Sites page using an <iframe> fairly difficult and styling the whole Visualforce page downright out of the question.  Moreover, different forms from the website have different lead sources and should associate with different campaigns in the database.

The solution was to create a Salesforce Sites form that will serve as an endpoint for an HTTP POST.  In my custom controller, I can easily retrieve parameters from the post, write them to the lead fields that I wish and create the lead, all contained within one controller method.  For each different web form, I can pass in different values for lead source, campaign, you name it!  Voilá, I am able to repurpose one custom web-to-lead form to POST to it from any form on my website.  The form does dual-purpose actually, not only can I POST to it, but it also acts a real web-to-lead form that could also be iframed into a web page.

First, let’s take a look at the Visualforce page:

<apex:page controller="ONEN_CTRL_WebToLead" action="{!insertLead}" showheader="false" sidebar="false" >
    <div style="display:none;"> 
    In addition to embedding this form directly into a page, you may also pass an HTTP POST directly into this form.
    The following labels map to POST parameters:
    * EXAMPLE Field Label - POST_Parameter_Name
    * Last Name (required) - Last_Name
    * First Name - First_Name
    * Organization - Organization
    * Email - Email
    * Mobile Phone - Mobile
    * Home Phone - Home_Phone
    * Lead Source - Lead_Source
    * Email Opt Out - Email_Opt_Out (must pass a string of 'true' to set this boolean)
    * Campaign Id - Campaign_Id
    * Campaign Member Status - Campaign_Member_Status
    * Home Street - Home_Street
    * Home City - Home_City
    * Home State - Home_State
    * Home Postal Code - Home_PostalCode
    </div>
    
    <apex:form >  
        <table>
                       
            <tr>
                <td align="right">
                    <apex:outputLabel value="First Name *" for="txtFirstName" style="font-weight:bold" />               
                </td>
                <td>
                    <apex:inputField value="{!lead.FirstName}" id="txtFirstName" required="true" />
                </td>
            </tr>
            <tr>
                <td align="right">
                    <apex:outputLabel value="Last Name *" for="txtLastName" style="font-weight:bold" />             
                </td>
                <td>
                    <apex:inputField value="{!lead.LastName}" id="txtLastName" required="true" />
                </td>
            </tr>
            <tr>
                <td align="right">
                    <apex:outputLabel value="E-Mail *" for="txtEmail" style="font-weight:bold" />              
                </td>
                <td>
                    <apex:inputField value="{!lead.Email}" id="txtEmail" required="true" />
                </td>
            </tr>
            <tr>
                <td align="right">
                    <apex:outputLabel value="Mobile" for="txtPostalCode" style="font-weight:bold" />               
                </td>
                <td>
                    <apex:inputField value="{!lead.MobilePhone}" id="txtMobilePhone" />
                </td>
            </tr>
            <tr>
                <td align="right">
                    <apex:outputLabel value="Home Street" for="txtStreet" style="font-weight:bold" />                
                </td>
                <td>
                    <apex:inputField value="{!lead.Home_Street__c}" id="txtStreet" />
                </td>
            </tr>           
            <tr>
                <td align="right">
                    <apex:outputLabel value="Home City" for="txtCity" style="font-weight:bold" />                
                </td>
                <td>
                    <apex:inputField value="{!lead.Home_City__c}" id="txtCity" />
                </td>
            </tr>
            <tr>
                <td align="right">
                    <apex:outputLabel value="Home State/Province" for="txtState" style="font-weight:bold" />             
                </td>
                <td>
                    <apex:inputField value="{!lead.Home_State__c}" id="txtState" />
                </td>
            </tr>
            <tr>
                <td align="right">
                    <apex:outputLabel value="Home Zip/Postal Code" for="txtPostalCode" style="font-weight:bold" />               
                </td>
                <td>
                    <apex:inputField value="{!lead.Home_PostalCode__c}" id="txtPostalCode" />
                </td>
            </tr>
            <tr>
                <td>
                <div style="display:none;">
                <apex:inputField value="{!lead.Campaign_Id__c}" id="txtCampaignId" />
                <apex:inputField value="{!lead.Campaign_Member_Status__c}" id="txtCampaignMemberStatus" />
                <apex:inputField value="{!lead.LeadSource}" id="txtLeadSource" />
                </div>
                </td>
                
                <td>
                    <apex:commandButton action="{!save}" value="Submit"/>&nbsp;&nbsp;&nbsp;<apex:outputLabel value="{!StrSaveResult}" />        
                </td>
            </tr>
        </table>
    </apex:form>
</apex:page>

And the controller. Notice how easy it is to just grab a parameter using the getParamters() method:

public class ONEN_CTRL_WebToLead {

	public Lead lead { 
		get {
			if (lead == null) lead = new Lead();
			return lead;
		}
		
		set; 
	}
	
	public PageReference Save() {
		if (lead.Company == null) lead.Company = '[not provided]'; 
		insert lead;
		StrSaveResult = 'Your information has been saved.  Thank you.';
		lead = null;	// so fields get reset to null.
		return null;
	}
	
	public String StrSaveResult { get; set; }
	
	public PageReference insertLead() {
		Map<string,string> postParameters = ApexPages.currentPage().getParameters();
		
		if (!postParameters.isEmpty()) {
			Lead newLead = new Lead();
			newLead.FirstName = ApexPages.currentPage().getParameters().get('First_Name');
			
			//hard code [not provided] for people without last names or company
			if (ApexPages.currentPage().getParameters().get('Last_Name')==null || ApexPages.currentPage().getParameters().get('Last_Name')=='') {
				newLead.LastName = '[not provided]';
			} else {
				newLead.LastName = ApexPages.currentPage().getParameters().get('Last_Name');
			}
			
			if (ApexPages.currentPage().getParameters().get('Organization')==null || ApexPages.currentPage().getParameters().get('Organization')=='') {
				newLead.LastName = '[not provided]';
			} else {
				newLead.Company = ApexPages.currentPage().getParameters().get('Organization');
			}
	
			newLead.Email = ApexPages.currentPage().getParameters().get('Email');
			newLead.Home_Phone__c = ApexPages.currentPage().getParameters().get('Home_Phone');
			newLead.MobilePhone = ApexPages.currentPage().getParameters().get('Mobile');
			newLead.LeadSource = ApexPages.currentPage().getParameters().get('Lead_Source');
			newLead.HasOptedOutOfEmail = ApexPages.currentPage().getParameters().get('Email_Opt_Out').contains('true');
			newLead.Campaign_Id__c = ApexPages.currentPage().getParameters().get('Campaign_Id');
			newLead.Campaign_Member_Status__c = ApexPages.currentPage().getParameters().get('Campaign_Member_Status');
			newLead.Home_Street__c = ApexPages.currentPage().getParameters().get('Home_Street');
			newLead.Home_City__c = ApexPages.currentPage().getParameters().get('Home_City');
			newLead.Home_State__c = ApexPages.currentPage().getParameters().get('Home_State');
			newLead.Home_PostalCode__c = ApexPages.currentPage().getParameters().get('Home_PostalCode');
			
			insert newLead;
		}
		
		return null;
	}
	
	//==================== TEST METHOD(s) ======================================
	static testmethod void CodeCoverageTests() {
		//instantiate the controller 
		ONEN_CTRL_WebToLead ctrl = new ONEN_CTRL_WebToLead();
		
		Lead lead = ctrl.lead;
		System.Assert(lead != null);
		lead.FirstName = 'TestFirstName';
		lead.LastName = 'TestLastName';
		ctrl.Save();
		System.AssertEquals('Your information has been saved.  Thank you.', ctrl.StrSaveResult);
	}	
}
Oct 8 09

A word of thanks

by Dave Manelski

I’d like to kickoff this new blog with a word of thanks to two important organizations without whom this blog (and my career for that matter) would not be possible — the Salesforce.com Foundation and CRMFusion.

The Salesforce.com Foundation enables thousands of non-profit organizations to tap into the Force.com platform to manage their CRM databases, custom applications and integrations. These powerful tools would otherwise be out of reach, cost-wise, for the clients that I work with every day.  I have attended a few Salesforce conferences & events and at each one CEO Marc Benioff promotes the 1% philanthropy model.  It’s a fantastic model, I wish more multi-national corporations were as generous.

CRMFusion’s DemandTools is another tool that I simply couldn’t live without.  A virtual Swiss Army knife of data migration and manipulation, DemandTools has saved me countless hours over the course of dozens of projects.  This must-have piece of software is free for non-profits.  I use it to wrestle data in and out of the database, make batch updates, and maintain data integrity with a de-duplication feature that is second to none.

At my organization, Groundwire, we’ve built our CRM consulting program on top of Salesforce.com, a decision that pays dividends with each platform upgrade release,  AppExchange product, and satisfied new client. It’s the generosity of the Salesforce.com Foundation and partners like CRMFusion that help make this possible.  Thank you and keep up the great work!

Sep 30 09

Hello World!

by Dave Manelski

Welcome to my new blog, check back often.