So I did a thing, Dynamic Email for Custom Modules through Actions Menu

Edit: I suppose is should note this is for 7.14. PHP 8

So I ran across a specific need and couldn’t really find any clear instructions on it, so I added the functionality myself. The following adds a button to the actions menu called Send Confirmation Emails and Sends and email to a customer for each related student in the subpanel.

For clarity, we use a module, Jobs, 1 record per course class. It has information like start date, job number, instructor, end date etc. It also has some relate fields which are used to retrieve data from within the following script. The courses module, instructors module, and customers module.

It then dynamically takes the information retrieved from those modules and replaces static placeholders in an email template (loaded using the template id). This process is a one click solution for emailing a single customer many confirmations. It can be adapted to do other things I just stopped here as my use case was complete.

So to start you will need a button.

open custom/YOUR MODULE/metadata/detailviewdefs.php

I have added my button in array value #5

      array (
        'buttons' => 
        array (
          0 => 'EDIT',
          1 => 'DUPLICATE',
          2 => 'DELETE',
          3 => 'FIND_DUPLICATES',
          4 => 
          array (
            'customCode' => '<input type="button" class="button" onClick="showPopup(\'pdf\');" value="{$MOD.LBL_PRINT_ROSTER_PDF}">',
		  5 =>
		  array (
			'customCode' => '<input type="button" class="button" onClick="sendStudentConfirmationEmails(\'{$}\');" value="Send Email Confirmations">',
      'maxColumns' => '2',

sendStudentConfirmationEmails is the EntryPointRegistry entry you will need to create. Instructions to do this are everywhere. will ensure the current record data is loaded when the button is clicked. You must be in the detail view of a record for this to work.

You will also need a little bit of javascript. Create a js folder. custom/YOUR MODULE/js
This will ensure your button is loaded upon page load.

I named my js file emailConfirmation.js
Make sure to change the entrypoint to whatever you created in the entrypoint registry

function sendStudentConfirmationEmails(recordId) {
    // AJAX call to custom endpoint for sending emails
        url: 'index.php?entryPoint=sendStudentConfirmationEmails&recordId=' + recordId,
        success: function(response) {
            alert('Emails sent successfully.');
        error: function() {
            // Handle error
            alert('Failed to send emails.');

In the custom/YOUR MODULE/views folder
open view.detail.php

You will need to add a pointer to your js file in the display function. THIS MUST BE IN THE DISPLAY FUNCTION

public function display()

echo '<script src="custom/modules/STTR_job/js/emailConfirmation.js"></script>';
//rest of your code

Then wherever you create your files, mine are in custom/YOUR MODULE/LogicHooks, lives the script that brings it all together. I’ve added comments throughout where I think they are necessary. I also kept it relatively lean.


if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

global $current_user, $db, $log;


if (isset($_REQUEST['recordId'])) {
    $jobId = $_REQUEST['recordId'];
    $emailer = new sendStudentConfirmationEmails();
    if ($emailer->sendConfirmationEmail($jobId)) {
        $log->fatal("Email processing completed for Job ID: {$jobId}.");
    } else {
        $log->fatal("Failed to process emails for Job ID: {$jobId}.");
} else {
    $log->fatal("No record ID provided to sendStudentConfirmationEmails entry point.");

class sendStudentConfirmationEmails
    public function sendConfirmationEmail($jobId)
        global $log;
        $templateId = '[template-id]'; // Your email template ID
        $log->warn("Sending confirmation emails for Job ID: {$jobId} using template ID: {$templateId}");
		//The following loads are specific to my use case for loading data from related modules, I kept them in to show how they are loaded
        $jobBean = BeanFactory::getBean('STTR_job', $jobId);
        if (empty($jobBean)) {
            $log->fatal("Job Bean could not be loaded.");
            return false;
		//Load specific customer record using the job module's relate field (stcus_customers_id_c)
        $customerBean = BeanFactory::getBean('STCUS_customers', $jobBean->stcus_customers_id_c);
        if (empty($customerBean)) {
            $log->fatal("Customer Bean could not be loaded.");
            return false;
		$customerName = $customerBean->name;  // The specific field I will be dynamically filling based on current record
		$courseBean = BeanFactory::getBean('STTR_courses', $jobBean->job_course_number_c);
        if (empty($courseBean)) {
            $log->fatal("Course Bean could not be loaded.");
            return false;
        $customerEmails = $this->getCustomerEmails($customerBean->id);
        if (empty($customerEmails)) {
            $log->fatal("No email found for customer with ID: {$customerBean->id}");
            return false;

		//When the Send Confirmation Email button is clicked in the job module, it will send an email for each student in the related subpanel
        $students = $this->getRelatedStudents($jobBean->id);
        $log->warn("Found " . count($students) . " related students for Job ID: {$jobId}");

        foreach ($students as $student) {
            foreach ($customerEmails as $customerEmail) {
                $log->warn("Attempting to send email to Customer: {$customerEmail} for Student ID: {$student->id}");
                if (!$this->sendEmailToCustomer($customerEmail, $student, $jobBean, $templateId)) {
                    $log->fatal("Failed to send email for student ID: {$student->id}");
                } else {
                    $log->warn("Email sent successfully for student ID: {$student->id}");

        return true;

    protected function getRelatedStudents($jobId)
        global $log;
        $jobBean = BeanFactory::getBean('STTR_job', $jobId);
        $relatedStudents = array();
		$log->warn("Loading customer bean with ID: '{$jobBean->stcus_customers_id_c}'");

        if ($jobBean->load_relationship('sttr_job_sttr_students_2')) {
            $relatedStudents = $jobBean->sttr_job_sttr_students_2->getBeans();
            $log->warn("Loaded " . count($relatedStudents) . " students via relationship.");
        } else {
            $log->fatal("Failed to load 'sttr_job_sttr_students_2' relationship for Job ID: {$jobId}");

        return $relatedStudents;

	//This function retrieves the primary email address for the "customer" in my case... Change STCUS_customers to your module name
    protected function getCustomerEmails($customerId)
        global $log, $db;
        $emails = [];
        $query = "SELECT ea.email_address FROM email_addresses ea 
                  INNER JOIN email_addr_bean_rel eabr ON = eabr.email_address_id 
                  WHERE eabr.bean_id = '{$customerId}' AND eabr.bean_module = 'STCUS_customers' AND eabr.deleted = 0 AND ea.deleted = 0";
        $result = $db->query($query);

        while ($row = $db->fetchByAssoc($result)) {
            $emails[] = $row['email_address'];

        return $emails;
	//Email Nuts and Bolts Function.  Use SugarMailer with system settings to send email
    protected function sendEmailToCustomer($customerEmail, $student, $jobBean, $templateId)
        global $log;
        $log->warn("Preparing to send email to customer: {$customerEmail} for student: {$student->id}");

        $emailData = $this->prepareEmailBody($student, $jobBean, $templateId);

        if (!$emailData['success']) {
            $log->fatal("Error preparing email: " . $emailData['message']);
            return false;

        $mail = new SugarPHPMailer();
        $admin = new Administration();

        $mail->From = $admin->settings['notify_fromaddress'];
		$mail->FromName = $admin->settings['notify_fromname'];
        $mail->Subject = $emailData['emailSubject'];
        $mail->Body = $emailData['emailBody'];
        $log->warn("Customer Email Address: '{$customerEmail}'");
		$mail->AddAddress(''); //Use this to send copies of email to another address statically

        $log->warn("Attempting to send email with subject: {$mail->Subject} to: {$customerEmail}");

        if (!$mail->Send()) {
            $errorInfo = $mail->ErrorInfo;
            $log->fatal("Could not send email to: $customerEmail, error: $errorInfo");
            return false;
        } else {
            $log->warn("Email successfully sent to: {$customerEmail}");

        return true;

    protected function prepareEmailBody($student, $jobBean, $templateId)
    global $log;

    // Retrieve instructor information from the related module
    $instructorBean = BeanFactory::getBean('Instructors', $jobBean->sttr_instructors_id_c);

    // If instructor information is not found, set placeholders to empty strings
    $instructorName = $instructorBean ? $instructorBean->first_name . ' ' . $instructorBean->last_name : '';

	 // Retrieve customer information from the related module
    $customerBean = BeanFactory::getBean('STCUS_customers', $jobBean->stcus_customers_id_c);
    // If customer information is not found, set customer name placeholder to empty string
    $customerName = $customerBean ? $customerBean->name : '';
	$coursesBean = BeanFactory::getBean('STTR_courses', $jobBean->sttr_courses_id_c);
	$courseNumber = $coursesBean ? $coursesBean->name : '';

    $emailTemplate = BeanFactory::getBean('EmailTemplates', $templateId);
    if (empty($emailTemplate)) {
        $log->fatal("Email template not found with ID: $templateId");
        return ['success' => false, 'message' => 'Email template not found.'];
	//The placeholders below will change to your record info.  make sure to include them exactly as named with brackets
    $placeholders = [
        '{{studentName}}' => $student->first_name . ' ' . $student->last_name . ' - ' . $student->student_id_number,
        '{{jobNumber}}' => $jobBean->name,
		'{{jobStart}}' => $jobBean->job_start_date_c,
		'{{jobEnd}}' => $jobBean->job_end_date_c,
        '{{instructorName}}' => $jobBean->sttr_instuctors_id_c,
        '{{customerName}}' => $customerName,
		'{{courseNumber}}' => $courseNumber,

    $emailBody = strtr($emailTemplate->body_html, $placeholders);
    $emailSubject = $emailTemplate->subject;

    $log->warn("Email body prepared for student ID: {$student->id}");

    return ['success' => true, 'emailBody' => $emailBody, 'emailSubject' => $emailSubject];


Set your log level to Warn so you don’t get bombarded by debugging hell!

For the email template I just used a table formatted how I liked it and used the placeholders in the script to dynamically add the data into the table where I preferred. Wherever the placeholder lives is where the data will be added. Note: The placeholder will be replaced with the record data, not added to, in case you don’t know how that works :slight_smile:

Hopefully, this helps some people get past a lot of the email questions I’ve seen. I’m on the forums every so often again after a hiatus doing a few years of network stuff, so I’ll be here to answer questions as time allows.

I’ll also be posting other interesting solutions as time permits.

Keep an eye out for pdf stuff. I basically split pdf functionality across multiple instances of tcpdf to customize the complete pdf experience per module so no goofy overlaps would screw up my output. That one is a little more extensive and will take some time to author instructions for.