+ Start a Discussion
LoganMooreLoganMoore 

Sending email from scheduled apex.

Hi Everyone,

 

I've run into an 'out of the box' problem that I could do with someone advice solving.

 

 

The situation

There's a third party system that inserts new order records (custom object) into salesforce through the API. The third party system may or may not insert one or more payment instruction records (a master-detail object of order) 90 seconds after the order is inserted. I've been asked to make a PDF order confirmation that gets emailed to the customer automatically when one of these orders is received.

 

The problem

The confirmation email needs to include payment instructions that we've received, but orders don't require payment instructions. Sometimes we get orders with no instructions initially. We might get those instructions much later on, but we still need to send a confirmation. This has two implications:

 

  1. I can't trigger this email from the order object because we haven't got the payment instructions by then so all confirmations will appear as if we haven't received the instructions.
  2. I can't trigger this email from the payment instruction object because if an order doesn't have payment instructions, we won't send a confirmation at all.

A failed solution

I thought I'd be smart and set up a scheduled apex task that runs every 5 minutes, and collects all orders, older than three minutes that haven't had a confirmation sent (using a checkbox to indicate whether the confirmation has been sent or not). This sounded like a decent hack on paper, but then I found out you can't send emails from scheduled apex. This fact alone has thrown my only remaining option on the fire.

 

 

Are there any ideas out there on how I could solve this problem?

Best Answer chosen by Admin (Salesforce Developers) 
LoganMooreLoganMoore

Hi Greg,

 

I actually stumbled upon the same article and managed to make it work for me. I wasn't going to post my solution so hastily this time in case it didn't work.

Basically, I had a visualforce page that generated a PDF, and wanted to include this in an email. Previously, I had used the PageReference and passed the id of the record I wanted to build the PDF from as a parameter. You can include a visualforce page in a visualforce email template, but you can't pass parameters to the page and an email template doesn't have a controller that it will pass to your pages constructor. This is why it wasn't going to work, and I imagine why you're having the same difficulty.

 

The solution is to use a component instead of a page for the PDF body. A component let's you pass custom attributes to the components controller when you call it. Here's a working example. I've omitted things specific to my use case.

 

 

Component Controller

Note that dealId is get/set variable. Also note that when I get the deal object, I first check if it's null, and have to query for it if it is. This is because you can't do the query on the set method of the ID variable, and this seemed like the most reliable alternative.
 

public class ConfirmationComponentController {
    
    public ID dealId {get;set;}
    
    public Deal__c deal;
    
    public Deal__c getDeal() {
        if (deal == null) {
            deal = [SELECT * FROM Deal__c WHERE Id = :dealId];
        }
        return deal;
    }
    
}

 
 

PDF Body Component

Note the attribute, you'll see how it's used int the email template.

<apex:component controller="ConfirmationComponentController" access="global">

    <apex:attribute name="dealId" description="The ID of a deal for which we want to generate a confirmation." assignTo="{!dealId}" type="Id" required="true" access="global" />

<h1>
Transaction Confirmation
<apex:outputText rendered="{!deal.Trade_Type__c = 'FXSPOT'}" value=" - Spot Deal" />
<apex:outputText rendered="{!deal.Trade_Type__c = 'FXFWD'}" value=" - Forward Contract" />
</h1>

/* ETC ETC */

</apex:component> 



VisualForce Email Template

Note that when you call the component, you get to pass in attributes.

<messaging:emailTemplate subject="Trade Confirmation - {!relatedTo.Name}" recipientType="Contact" relatedToType="Deal__c">
    <messaging:htmlEmailBody >
        <h1>Testing</h1>
    </messaging:htmlEmailBody>
    <messaging:plainTextEmailBody >
        Testing
    </messaging:plainTextEmailBody>
    <messaging:attachment filename="Velocity Trade Confirmation - {!relatedTo.Name}" renderAs="PDF">
        <c:Confirmation dealId="{!relatedTo.Id}" />
    </messaging:attachment>
</messaging:emailTemplate>

 

Plain Old PDF Page

Now that we've got the PDF working in the email, to save doubling our efforts by maintaing a PDF page as well, I'm going to write a page that renders as PDF, but only includes the PDF Body component.



Another way of doing things:

 

My solution is based on passing the deal ID to the component, and letting the component query the information it needs, but there is another way. You can pass objects through attributes as well, so I was able to pass {!relatedTo} as the value of a Deal__c attribute, and my controller simply stored the deal in it's deal get/set variable.

This is arguably, much simpler, but  it meant that whereever I used the component I needed to make sure I queried the deal with the correct fields before invoking the component. For simplicity sake, I'm going to let my component take care of all its own queries so I can be 100% sure it has what it needs.

All Answers

LoganMooreLoganMoore

I think I've figure out a potential solution. Going to test it tonight.


1. Make trigger on order insert that generates the pdf and saves as an attachment on the order.

2. Make trigger on payment instruction that generates a new pdf and over writes the existing pdf generated by the order insert trigger.

3. Schedule some apex that runs every 5 minutes, and sends an email using the pdf that's attached to the order, instead of trying to generate the pdf in the scheduled apex code.


This should work around the limitation of not being able to use getContent or getContentAsPDF in scheduled or future methods.

LoganMooreLoganMoore

Bah! Humbug!

This didn't work. I'm desperate to solve this problem now.

Greg RohmanGreg Rohman

Hello.

 

I'm having a similar issue, where I'm looking to generate a PDF at a set interval (scheduled). The limitations of the scheduler in not being able to use the getContent methods is making things difficult.

 

There's a method posted at the following link  that provides a possible workaround: http://wiki.developerforce.com/index.php/Visualforce_EmailQuote2PDF

 

I'm unable to figure a way to get this method to work for my own purposes, though, as it seems that it will only work for standard controllers. I'm curious if you find a way to accomplish what you need.

 

-Greg

LoganMooreLoganMoore

Hi Greg,

 

I actually stumbled upon the same article and managed to make it work for me. I wasn't going to post my solution so hastily this time in case it didn't work.

Basically, I had a visualforce page that generated a PDF, and wanted to include this in an email. Previously, I had used the PageReference and passed the id of the record I wanted to build the PDF from as a parameter. You can include a visualforce page in a visualforce email template, but you can't pass parameters to the page and an email template doesn't have a controller that it will pass to your pages constructor. This is why it wasn't going to work, and I imagine why you're having the same difficulty.

 

The solution is to use a component instead of a page for the PDF body. A component let's you pass custom attributes to the components controller when you call it. Here's a working example. I've omitted things specific to my use case.

 

 

Component Controller

Note that dealId is get/set variable. Also note that when I get the deal object, I first check if it's null, and have to query for it if it is. This is because you can't do the query on the set method of the ID variable, and this seemed like the most reliable alternative.
 

public class ConfirmationComponentController {
    
    public ID dealId {get;set;}
    
    public Deal__c deal;
    
    public Deal__c getDeal() {
        if (deal == null) {
            deal = [SELECT * FROM Deal__c WHERE Id = :dealId];
        }
        return deal;
    }
    
}

 
 

PDF Body Component

Note the attribute, you'll see how it's used int the email template.

<apex:component controller="ConfirmationComponentController" access="global">

    <apex:attribute name="dealId" description="The ID of a deal for which we want to generate a confirmation." assignTo="{!dealId}" type="Id" required="true" access="global" />

<h1>
Transaction Confirmation
<apex:outputText rendered="{!deal.Trade_Type__c = 'FXSPOT'}" value=" - Spot Deal" />
<apex:outputText rendered="{!deal.Trade_Type__c = 'FXFWD'}" value=" - Forward Contract" />
</h1>

/* ETC ETC */

</apex:component> 



VisualForce Email Template

Note that when you call the component, you get to pass in attributes.

<messaging:emailTemplate subject="Trade Confirmation - {!relatedTo.Name}" recipientType="Contact" relatedToType="Deal__c">
    <messaging:htmlEmailBody >
        <h1>Testing</h1>
    </messaging:htmlEmailBody>
    <messaging:plainTextEmailBody >
        Testing
    </messaging:plainTextEmailBody>
    <messaging:attachment filename="Velocity Trade Confirmation - {!relatedTo.Name}" renderAs="PDF">
        <c:Confirmation dealId="{!relatedTo.Id}" />
    </messaging:attachment>
</messaging:emailTemplate>

 

Plain Old PDF Page

Now that we've got the PDF working in the email, to save doubling our efforts by maintaing a PDF page as well, I'm going to write a page that renders as PDF, but only includes the PDF Body component.



Another way of doing things:

 

My solution is based on passing the deal ID to the component, and letting the component query the information it needs, but there is another way. You can pass objects through attributes as well, so I was able to pass {!relatedTo} as the value of a Deal__c attribute, and my controller simply stored the deal in it's deal get/set variable.

This is arguably, much simpler, but  it meant that whereever I used the component I needed to make sure I queried the deal with the correct fields before invoking the component. For simplicity sake, I'm going to let my component take care of all its own queries so I can be 100% sure it has what it needs.

This was selected as the best answer
Greg RohmanGreg Rohman

Hi Logan.

 

Thank you very much. This worked perfectly! And, as you had mentioned, I replaced my standard PDF page with a simple container page that pulls in the component.

 

I had seen a couple of other threads on the forums that were attempting to accomplish the same thing. I'll post a reference to your posting here.

 

Thanks again.

 

-Greg

goabhigogoabhigo

Now how are you sending email? Is it through batch apex or manually selecting VF template in standard email window?

 

Please let me know, as I am running into similar kind of problem and been trying out different logics.

Greg RohmanGreg Rohman

Hi Abhi.

 

I'm sending it via Batch Apex.  Basically, I loop on all of the members of a particular group that I want to send the message to, and send each an individual message. Here's my simplified code below, including a test method.

 

global class ClsSendPromoInventoryRptScheduled implements Schedulable {
  global void execute(SchedulableContext sc) {

    // First, need to obtain the ID of the email template we created.
    EmailTemplate et = [SELECT Id FROM EmailTemplate WHERE DeveloperName='Promotional_Stationery_Inventory'];

    // Due to "MassEmailMessage" not supporting templates, we have to loop on the recipients and send them a single message.
    // Get a list of all of the users in the "Promotional Item Email Report Recipients" group
    List<GroupMember> groupMemberList = [SELECT UserOrGroupId FROM GroupMember WHERE GroupId=:[SELECT Id FROM Group WHERE Name='Promotional Item Email Report Recipients']];
    for (GroupMember gm : groupMemberList) {
      Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
      mail.setTemplateId(et.Id);
      mail.setSaveAsActivity(false);
      mail.setTargetObjectId(gm.UserOrGroupId);
          Messaging.SendEmailResult [] r = Messaging.sendEmail(new Messaging.SingleEmailMessage[] {mail});
    }

  }


    public static testMethod void tester() {  
        String CRON_EXP = '0 0 0 3 9 ? 2022';
        Test.startTest();
        // Schedule the test job  
        String jobId = System.schedule('testSendPromoReportEmail',CRON_EXP, new ClsSendPromoInventoryRptScheduled());
        // Get the information from the CronTrigger API object  
        CronTrigger ct = [SELECT id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE id = :jobId];
        // Verify the expressions are the same  
        System.assertEquals(0, ct.TimesTriggered);
        // Verify the next time the job will run  
        System.assertEquals('2022-09-03 00:00:00', String.valueOf(ct.NextFireTime));
        Test.stopTest();


    }

}

 

Hope that helps.

 

-Greg

LoganMooreLoganMoore

This is how I wanted to kick off emails, but the most frequent you can schedule a task to occur is once an hour.

 

Instead, I wrote a web service method called "everyFiveMinutes" and I've got a python script on one of our linux servers thats run every five minutes by cron and will call the everyFiveMinutes method. My scheduled tasks are effectively offloaded to another machine :D.

goabhigogoabhigo

Thanks for the reply.

 

Ok so basically you are selecting template. In my case I need to attach an excel to the mail. getContent() is not supported by scheduler :(

 

Hey, you have written sendEmail() method inside for loop. It will hit the limits (think its 10, i.e. you will be able to send 10 emails in one shot).

Greg RohmanGreg Rohman

Hi Abhi.

 

Calling sendEmail inside the loop will definitely hit limits, and might not be considered a best practice. But for this particular case, I am only sending to a very small number of individuals in a group that I control. For larger sends, I would have to rethink this a bit. 

 

In regards to attaching an Excel, I'm simply using renderas="pdf" in my messaging attachment and including the component. See below:

 

<messaging:attachment filename="Attachment name here" renderAs="PDF">
<c:PromotionalInventory />
</messaging:attachment>

 

Have you tried using "application/x-excel" instead of "pdf" in the renderas?

 

Logan, nice solution!

 

 

-Greg

goabhigogoabhigo

Thanks Greg for helping.

 

I really don't understand what I am missing, can you please look into this.

 

// currently using this class to test. I have a VF related to this which shows table of Key records whose details to be sent


public class RenewalEmailSender_UsingTemplate {

    public List<Key__c> lstKeys {get; set;}
    private Map<String,Key__c> mapKeysUnique;
    private String[] toAddresses;
        
    Transient Messaging.SingleEmailMessage mail;    
    Transient Messaging.EmailFileAttachment efa;
    Transient Messaging.SendEmailResult [] r;
    Transient List<Messaging.SingleEmailMessage> sem;
    Transient Set<ID> targetIdsFailed;
    
    public Boolean test;
    
    public RenewalEmailSender_UsingTemplate(ApexPages.StandardController controller) {
    }
    public RenewalEmailSender_UsingTemplate() {
    }
        
    public void initialize() {
        test=false;
        mapKeysUnique = new Map<String,Key__c> ();
        lstKeys = [select Name,Reseller__c,Maintenance_Renewal_Date__c,OwnerId,Reseller__r.Name,End_User__r.Name,
                          Maintenance_Renewal_reminder_sent_date__c,Email_ID_of_Reseller__c,Owner.Name,
                          Opportunity__r.Account.Name,SKU__c,Product__c,Quantity__c,Reseller_Contact_Id__c,
                          Product_Key_V3__c,Product_Key_V4__c,Status__c,Maintenance_Renewal_Price__c 
                          FROM Key__c WHERE
                          Maintenance_Renewal_Date__c > NEXT_90_DAYS AND Maintenance_Renewal_Date__c <= NEXT_N_DAYS:120
                          ORDER BY Reseller__c];
        for(Key__c k : lstKeys) {
            if(k.Email_ID_of_Reseller__c != null)
                mapKeysUnique.put(k.Email_ID_of_Reseller__c, k);
        }
    }
        
    public void sendMail() { // this method is called on button click
        sem = new List<Messaging.SingleEmailMessage> ();
        if(! mapKeysUnique.values().isEmpty()) {
            for(Key__c k : mapKeysUnique.values()) {
                sendAndAttach(k);
            }

            sendInBulk();
            update lstKeys;
        }
    }
    
    private void sendAndAttach(Key__c k) {
        System.debug('### (RenewalEmailSender) k: ' + k);
        mail = new Messaging.SingleEmailMessage();
        
        if(k.Email_ID_of_Reseller__c == null) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL,'Error while sending email - No TO address specified ('+
                                 k.Name));
            return;
        }    
       
        mail.setSubject('Maintenance Renewal for next 90 days (' + System.Today() + ')');
        mail.setTargetObjectId(k.Reseller_Contact_Id__c);
        mail.setTemplateId('00XR0000000IH56');
        sem.add(mail);
        for(Key__c k1: lstKeys) {
            if(k.Reseller__c == k1.Reseller__c)
                k1.Maintenance_Renewal_reminder_sent_date__c = System.Today();
        }
    }
    
    private void sendInBulk() {
        r = Messaging.sendEmail(sem,false);
    }
}

 

The VF email template

<messaging:emailTemplate subject="90 Day Maintenance Renewal Notice (Reseller)" recipientType="Contact" relatedToType="Contact">
    <messaging:plainTextEmailBody >
        NOW IS THE TIME FOR YOUR CUSTOMER'S PRODUCT MAINTENANCE RENEWAL...............
    </messaging:plainTextEmailBody>
    <messaging:htmlEmailBody >
        <h2><b>Now is the time for your customer's products maintenance renewal.............</b></h2>
    </messaging:htmlEmailBody>
    <messaging:attachment filename="Maintenance Renewal Notice - {!relatedTo.Name}" renderAs="PDF">
        <c:MaintenanceRenewalComponent contId="{!relatedTo.Id}" />
    </messaging:attachment>
</messaging:emailTemplate>

 Th VF component:

<apex:component controller="MaintenanceKeyController" access="global">
    <apex:attribute name="contId" description="The ID of the Reseller for whome we want to send email" assignTo="{!contId}" type="Id" required="true" access="global" />
    <table border="1" cellspacing="0">
        <tr>
            <th style="background: #BABABA;"> End User </th>
            <th style="background: #BABABA;"> Maintenance Renewal Date </th>
            <th style="background: #BABABA;"> Product </th>
        </tr>
        <apex:repeat value="{!lstKey}" var="key">
            <tr>
                <!-- Some text here -->
                <td> 
                    {!key.End_User__r.Name} 
                </td>
                <td>
                    <apex:outputField value="{!key.Maintenance_Renewal_Date__c}"/>
                </td>
                <td>
                    <apex:outputField value="{!key.Product__c}"/>
                </td>
            </tr>
        </apex:repeat>
    </table>
</apex:component>

 And it's controller:

public class MaintenanceKeyController {
    public List<Key__c> lstKey;
    public ID contId {get;set;}
    
    public List<Key__c> getlstKey() {
        if(lstKey==null)
            lstKey = [select End_User__r.Name,Maintenance_Renewal_Date__c,Product__c from Key__c where Reseller_Contact_Id__c =: contId AND
                 Maintenance_Renewal_Date__c > NEXT_90_DAYS AND Maintenance_Renewal_Date__c <= NEXT_N_DAYS:120
                 AND Maintenance_Renewal_reminder_sent_date__c = null];
        System.debug('-------- contId: ' + contId);
        System.debug('-------- lstKey: ' + lstKey);        
        return lstKey;
    }
    }
}

 

I checked it over and over, but couldn't find the mistake. The email is not received that is the only thing I could find.

 

Any help will be greatly appreciated, as this is very important for my implementation.

 

Greg RohmanGreg Rohman

Hi Abhi.

 

I can't see anything obviously wrong with the code, but I do have some questions.

 

  • In sendAndAttach(), Does Reseller_Contact_Id__c contain the ID of a valid contact who has not opted out of emails, and does that contact have an email address? You have a conditional statement checking for Email_ID_of_Reseller__c, but your passing Reseller_Contact_Id__c to setTargetObjectId, which you need to do. 
  • Is the ID you've specified for the email template a valid template ID? It might be better to query for the template ID based on it's name, so that you don't have potential issues with different IDs when deployed to your production server.
  • What is contained in your SendEmailResult r variable after the send?
  • Are you able to manually (not via Apex) send emails from Salesforce.com using templates? Check this to make sure there isn't some other requirement that's preventing the mail from sending.

 

-Greg

 

goabhigogoabhigo

Thanks Greg.

 

The ID I have specified is valid contact and Email Opt Out is not checked.

 

Here is the debug log:

 

The 'mail' content:

Messaging.SingleEmailMessage[getBccAddresses=null;getCcAddresses=null;getCharset=null;getDocumentAttachments=null;getFileAttachments=null;getHtmlBody=null;getInReplyTo=null;getOrgWideEmailAddressId=null;getPlainTextBody=null;getReferences=null;getTargetObjectId=003R000000YOmRwIAL;getTemplateId=00XR0000000IH56MAG;getToAddresses=(abhilash023+sctest_reseller2_1@gmail.com);getWhatId=003R000000YOmRwIAL;isUserMail=false;]

 

SendEmailResult 'r':

(Messaging.SendEmailResult[getErrors=(Messaging.SendEmailError[getTargetObjectId=null;]);isSuccess=false;], Messaging.SendEmailResult[getErrors=(Messaging.SendEmailError[getTargetObjectId=null;]);isSuccess=false;])

 

 

goabhigogoabhigo

Also, when I select this template in UI, the PDF is just with table headers, no data in it !!

 

But when I use the component in VF page, the table is displayed related particular Account (or Reseller).

goabhigogoabhigo

Now I am able to send email from Salesforce (not using apex). Modified component controller code..

Greg RohmanGreg Rohman

I"m trying to eliminate possible causes of the failure, so I'd like to remove the sendInBulk method for now. For testing purposes, can you try moving you messaging.sendEmail out of the sendInBulk method and into the sendAndAttach method? Something like this:

 

        // code above here removed for clarity
        mail.setSubject('Maintenance Renewal for next 90 days (' + System.Today() + ')');
        mail.setTargetObjectId(k.Reseller_Contact_Id__c);
        mail.setTemplateId('00XR0000000IH56');
        r = Messaging.sendEmail(mail,false);

 

-Greg