I have an email handler class which takes failure notification emails from a payment gateway and updates the relevant records in Salesforce accordingly. The payment gateway sends these emails in an overnight batch, so we may get more than one email at the same time. Recently we had our first instance of multiple failure emails, and only one of the emails triggered the expected process.

Here's the class:

global class EmailHandlerPayments implements Messaging.InboundEmailHandler {

global Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope envelope) {
    Messaging.InboundEmailResult result = new Messaging.InboundEmailresult();
    String lptId;
    String loanId;
    String debRef;

    try {
        if(email.subject.startsWithIgnoreCase('Debit Failed transactions')) {
            system.debug('### Failed payment email received: ' + email.subject);
            system.debug('### html body: ' + email.htmlBody);
            system.debug('### plain text body: ' + email.plainTextBody);
            // Regex search for loanId+lptid
            Pattern myPattern = Pattern.compile('(\\d{9})-(LPT-\\d{9})');
            Matcher myMatcher = myPattern.matcher(email.htmlBody);
            if (myMatcher.find()) {
                loanId = myMatcher.group(1);
                lptId = myMatcher.group(2);
            } else system.debug('### Failed to find correct string in email');
            system.debug('### parsed loan id ' + loanId + ' and LPT ' + lptId);

            // Regex search for debit reference url
            myPattern = Pattern.compile('p.vu\\/sr\\/(\\d{8})');
            myMatcher = myPattern.matcher(email.htmlBody);
            if (myMatcher.find()) {
                debRef = myMatcher.group(1);
            system.debug('### debit ref: ' + debRef);
                try {
                    rejectLPT(lptId, loanId, debRef);
                    system.debug('### rejectLPT(' +lptId+','+loanId+','+debRef+')');
                    result.success = true;
                } catch (Exception e) {
                    result.success = false;
                    result.message = 'ERROR: failed to find matching lpt ' +lptId+ ' and loan ' +loanId+ ': line '+e.getLineNumber()+' - ' +e.getMessage();


    } catch (Exception e) {
        result.success = false;
        result.message = 'EmailHanderPayments failure at line number - '+e.getLineNumber()+'\n'+e.getMessage();

    return result;

public static void rejectLPT (String lptId, String loanId, String debRef){
    // Find and update related Loan Payment Transaction and CL Contract
    boolean updateCLC = FALSE;
    boolean updateLPT = FALSE; 
    loan__Loan_Account__c relCLContract = [SELECT Name, Id, Banjo_Loan_No__c, loan__la_delinquency_flag__c,
        loan__Loan_Status__c FROM loan__Loan_Account__c WHERE Banjo_Loan_No__c=:loanId];
    loan__Loan_Payment_Transaction__c failedLPT = [SELECT Name, Id, loan__Rejected__c, loan__Rejection_Reason__c, 
        loan__Loan_Account__c FROM loan__Loan_Payment_Transaction__c WHERE Name=:lptId 
        AND loan__Loan_Account__c=:relCLContract.id];
    system.debug('### Matching Loan Payment Transaction: ' + failedLPT.id +' '+failedLPT.name);
    if(failedLPT.loan__Rejected__c==FALSE) {
        updateLPT = TRUE;
    if(failedLPT.loan__Rejection_Reason__c==null) {
        failedLPT.loan__Rejection_Reason__c='Payment Adviser failure notification received, debit reference '+debRef;
        updateLPT = TRUE;
    if(relCLContract.loan__la_delinquency_flag__c==FALSE) {
        updateCLC = TRUE;
    if(relCLContract.loan__Loan_Status__c!='Active - Bad Standing') {
        relCLContract.loan__Loan_Status__c='Active - Bad Standing ';
        updateCLC = TRUE;
    loan__Charge__c lateFee = new loan__Charge__c(loan__Date__c=date.today(),loan__Reference__c='Failure of debit reference '+debRef,loan__Loan_Account__c=relCLContract.Id);
    lateFee.loan__Fee__c=[select Name from loan__Fee__c where Name=:'Late Fee' AND loan__State__c=:'Active' LIMIT 1].Id;
    insert lateFee;
    if (updateCLC==TRUE) update relCLContract;
    if (updateLPT==TRUE) update failedLPT;

    // Get list of active phone numbers to alert
    list<SMS_Alert_Recipients__c> alertNos = SMS_Alert_Recipients__c.getall().values();
    list<string> smsNos = new list<string>();
    for (SMS_Alert_Recipients__c person : alertNos){
        if(person.Active__c == TRUE && person.Payment_fail_opt_in__c == TRUE) smsNos.add(string.valueof(person.mobile_Number__c)); 

    // create SMS record to send
    if(smsNos.size() > 0){ 
        SMSTemplate sms = new SMSTemplate();
        List<smagicinteract__smsMagic__c> failSmsList = new List<smagicinteract__smsMagic__c>();
        failSmsList.addAll(sms.createSMSTemplate(new Id[]{failedLPT.Id,relCLContract.Id}, 'Debit Failed Internal Notification', smsNos));
        Database.insert(failSmsList, false); 
    } else system.debug('### No active SMS recipients found!');

I've tried recreating this with the following in an execute anonymous window, but both records were updated correctly.

  // Create a new email and envelope object
Messaging.InboundEmail email1  = new Messaging.InboundEmail();
Messaging.InboundEnvelope env1 = new Messaging.InboundEnvelope();
Messaging.InboundEmail email2  = new Messaging.InboundEmail();
Messaging.InboundEnvelope env2 = new Messaging.InboundEnvelope();
// Set up your data if you need to

// Create the email body
string payRef1 = '11111111';
email1.htmlBody = 'wefwef111111121-LPT-000000341wfwefwgertg p.vu/sr/'+payRef1+'wewefwefwef';
system.debug('### test email body: ' + email1.htmlBody);
email1.fromAddress ='[email protected]';
email1.subject = 'Debit Failed transactions test email';
string payRef2 = '11111112';
email2.htmlBody = 'wefwef201603031-LPT-000000342wfwefwgertg p.vu/sr/'+payRef2+'wewefwefwef';
system.debug('### test email body: ' + email2.htmlBody);
email2.fromAddress ='[email protected]';
email2.subject = 'Debit Failed transactions test email';

EmailHandlerPayments ehp = new EmailHandlerPayments();

Messaging.InboundEmailResult result1 = ehp.handleInboundEmail(email1, env1);
Messaging.InboundEmailResult result2 = ehp.handleInboundEmail(email2, env2);

system.debug('### email 1: ' + result1);
system.debug('### email 2: ' + result2);

Can I resolve this through some sort of bulkifying? I didn't add any as most email handler examples I've seen don't have any and I assumed the email service would process emails sequentially.

Appreciate any advice.

  • 1
    Did you received any error? Mar 3, 2016 at 7:02
  • As above, error messages would definitely help. One quick thing to check is the rate limit. Email Services allow 1000 emails per SF licence per day (developer.salesforce.com/docs/atlas.en-us.apexcode.meta/…). Also, I'm not sure I'd rely on unsecured email as a way of receiving payment info, but that's a whole other topic.
    – Aidan
    Mar 3, 2016 at 10:39
  • 1
    There is no concept of bulkification for inbound email services - each email is processed 1x1; more likely, you are getting a concurrency error (record lock issue) and should see if you can work around that with appropriate SFDC queries for update
    – cropredy
    Mar 6, 2016 at 8:36
  • It's taken a while but I've finally got logs of this happening. All subsequent emails processed after the first one in this scenario result in an UNABLE_TO_LOCK_ROW error. The ID given for this error is a record I'm querying to populate a lookup on a record created by the handler. Seems that since email handlers don't process in bulk, if more than one queries the same record it can result in this UNABLE_TO_LOCK_ROW exception. I've looked around and seen a suggestion to use the "FOR UPDATE" keyword at the end of the relevant SOQL query here. Is this the best way to resolve this? Apr 4, 2016 at 6:28

1 Answer 1


Following up on my last comment, it was indeed the UNABLE_TO_LOCK_ROW issue and adding FOR UPDATE to the relevant SOQL query seems to resolve the issue:

[SELECT Name FROM Loan__Fee__c where Name=:'Late Fee' 
 AND Loan__State__c =: 'Active' LIMIT 1 FOR UPDATE]

It's bizarre that Salesforce would lock an object on query rather than on DML, as the record being queried does not need to be updated at all in this code so locking should be unnecessary.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .