> For the complete documentation index, see [llms.txt](https://docs.valorx.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.valorx.com/valorx-wave/wave-advanced/send-an-email-using-row-action.md).

# Send an Email using Row Action

A simple guide to the Send Email button using row action button.

Wave Embedded’s Row Action feature makes it easy to add useful actions directly to your grids. One of the ways you can use it is to add a **Send Email** button, giving users a quick way to send messages without leaving the grid.

When clicked, the button opens a familiar email window where the recipient is already populated. You can choose a ready-made template, add attachments, and send the email all from the same page.

This guide walks you through how to set up the **Send Email** button using Row Actions.

<figure><img src="https://2237973035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F65f4C4H8V5e0YV342mwp%2Fuploads%2F97FuqcPg095W4vvO9K5w%2FSend%20Email%20v2.gif?alt=media&amp;token=f69f62f3-8394-4fe1-a144-33e9bacbffaa" alt=""><figcaption></figcaption></figure>

## Before You Start

Row Actions are available in Wave Embedded. Before adding the **Send Email** button, check the following:

* **Download the Send Email** component and deploy it to your Salesforce org. Once added, it will be available as an option in the Row Action wizard.
* **Set up at least one email address** for your company to send emails from.
* **Activate the email templates** you want your team to use. Only active templates appear in the template list.

{% file src="/files/vBD3lHgtaY9Dmi7v5W13" %}

### What the Send Email Component Supports

The Send Email component provides the following functionality:

* **Default To address** for Contact and Lead grids.
* **Default From address** based on organization-level configuration.
* **CC and BCC** recipient support.
* **Email template** selection.
* **Related To** populated for objects other than Contact and Lead.
* **Attachment** support for outgoing emails.

## Configure the Row Action on the Contact Grid

### Configuration Steps

{% stepper %}
{% step %}

#### Adding the Button

* Select an existing Contact grid or create a new Contact grid.
* Go to the **Actions** tab.

<figure><img src="https://2237973035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F65f4C4H8V5e0YV342mwp%2Fuploads%2FGKMMTdpI9sBFFLYQ3gap%2F1.%20Edit%20Action.gif?alt=media&amp;token=bcfb432b-cb7c-4659-aa7f-2dfc1fa93174" alt=""><figcaption><p>Go to Actions tab</p></figcaption></figure>

* Click **+ Row Action** button.
* Enter a button label and tooltip, such as **Send Email**.
* Click **Next**.

<figure><img src="https://2237973035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F65f4C4H8V5e0YV342mwp%2Fuploads%2FRNPD4BWDRBkES4iYOfEP%2F2.%20Styling.gif?alt=media&amp;token=0b932dd0-e35b-4d21-9837-7d3a8b541ed5" alt=""><figcaption><p>Set the Label and Tooltip</p></figcaption></figure>
{% endstep %}

{% step %}

#### Connect the Button to the LWC Component

* In the **Select Salesforce Component** dropdown, select **LWC**.
* Search for and select the **sendEmailCustom** component deployed in the prerequisite step.
* Enter the success message to display after the email is sent.
* Click **Next**.

<figure><img src="https://2237973035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F65f4C4H8V5e0YV342mwp%2Fuploads%2FvcLH3JTbHNZeBHbn2evL%2F2.%20Action.gif?alt=media&amp;token=88f6cebf-288a-47f4-8bea-22070d3f349d" alt=""><figcaption><p>Connect the Button to the LWC Component</p></figcaption></figure>
{% endstep %}

{% step %}

#### Set the Variable and Save

* Click **+ Add Variable** and configure the variable as follows:
  * **Name:** `recordId`
  * **Type:** `String`
  * **Parameters:** `Dynamic Field`
  * **Dynamic Field:** `Contact Id`
* Click **Save and Finish**.

<figure><img src="https://2237973035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F65f4C4H8V5e0YV342mwp%2Fuploads%2FPOjAFFcRSQhjQkOBnQBr%2F3.%20Set%20the%20Variable%20and%20Save.gif?alt=media&amp;token=391dda1f-5326-48f5-a1e5-4357214872b1" alt=""><figcaption><p>Set the Variable and Save</p></figcaption></figure>

**Note:** To add the same row action to another object grid, repeat the configuration steps and select the appropriate record ID field for that object in **Dynamic Field**.
{% endstep %}
{% endstepper %}

***

## What Your Team Will See

When a user clicks the **Send Email** button, the email window opens directly over the grid.

For Contact and Lead grids, the **To** address is populated automatically when an email address is available. Users can also add additional recipients, select a **From** address, choose an email template, edit the message, add **Cc/Bcc** recipients, and attach files.

When the email is ready, click **Send**. The email is sent, the configured success message appears, and the email window closes.

<figure><img src="https://2237973035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F65f4C4H8V5e0YV342mwp%2Fuploads%2F18JBRFqQg5iD2e3Qzk56%2FSend%20Email.gif?alt=media&amp;token=dfcaa52c-e690-4e55-a7ef-9e9b079d8181" alt=""><figcaption></figcaption></figure>

## Source Code

The source code used for the **Send Email** component is provided below. You can refer to it, learn from it, or use it as a starting point for a similar implementation.

Using or modifying the code may require knowledge of Salesforce development and Lightning Web Components (LWC).

**Note:** The source code is provided as-is and has not been edited, reformatted, or otherwise modified for this guide.

{% file src="/files/vBD3lHgtaY9Dmi7v5W13" %}

### Apex Class

<details>

<summary>classes/SendEmailController.cls</summary>

{% code overflow="wrap" %}

```
/**
 * SendEmailController
 * -------------------
 * Server-side controller for the `sendEmail` LWC that replicates the
 * standard Salesforce "Send an Email" composer, invoked from a Valorx
 * grid Row Action.
 *
 * Responsibilities:
 *   - Resolve the recipient (Name / Email) from the row record dynamically.
 *   - Provide Org-Wide Email Addresses for the "From" picker.
 *   - Provide active Email Templates and render them (merge-field resolution
 *     via Messaging.renderStoredEmailTemplate).
 *   - Send the email (Messaging.SingleEmailMessage) with CC/BCC, attachments,
 *     and optional "save as activity" logging on the record.
 *
 * NOTE (managed package): in the `valorxwave` 2GP package the namespace is
 * applied automatically at package time, so source references stay un-namespaced.
 */
public with sharing class SendEmailController {

    private static final Set<String> MESSAGEABLE_OBJECTS =
        new Set<String>{ 'Contact', 'Lead', 'User' };

    // ---------------------------------------------------------------------
    // Wrapper DTOs (returned to / received from the LWC)
    // ---------------------------------------------------------------------
    public class EmailContext {
        @AuraEnabled public String recipientName { get; set; }
        @AuraEnabled public String recipientEmail { get; set; }
        @AuraEnabled public String objectApiName  { get; set; }
        @AuraEnabled public Boolean isMessageable  { get; set; } // Contact/Lead/User => can log activity + resolve merge fields
        @AuraEnabled public List<Option> orgWideAddresses  { get; set; }
        @AuraEnabled public String nameField { get; set; }
    }

    public class Option {
        @AuraEnabled public String label;
        @AuraEnabled public String value;
        public Option(String label, String value) { this.label = label; this.value = value; }
    }

    public class RenderedTemplate {
        @AuraEnabled public String subject;
        @AuraEnabled public String htmlBody;
    }

    public class EmailRequest {
        @AuraEnabled public String recordId {get;set;}
		@AuraEnabled public List<String> toAddresses {get;set;}
		@AuraEnabled public List<String> ccAddresses {get;set;}
		@AuraEnabled public List<String> bccAddresses {get;set;}
		@AuraEnabled public String subject {get;set;}
		@AuraEnabled public String htmlBody {get;set;}
		@AuraEnabled public String orgWideEmailAddressId {get;set;}
		@AuraEnabled public Boolean saveAsActivity {get;set;}
		@AuraEnabled public String targetObjectId {get;set;}
		@AuraEnabled public List<String> contentDocumentIds {get;set;}
        @AuraEnabled public Id whatId { get; set; }
    }

    // ---------------------------------------------------------------------
    // Context: recipient + From addresses
    // ---------------------------------------------------------------------
    @AuraEnabled(cacheable=true)
    public static EmailContext getEmailContext(Id recordId, String emailFieldOverride) {
        EmailContext ctx = new EmailContext();

        Schema.DescribeSObjectResult dsr = recordId.getSObjectType().getDescribe();
        ctx.objectApiName = dsr.getName();
        ctx.isMessageable = MESSAGEABLE_OBJECTS.contains(ctx.objectApiName);

        Map<String, Schema.SObjectField> fieldMap = dsr.fields.getMap();
        ctx.nameField = findNameField(fieldMap);
        // Resolve the email field: explicit override (validated) -> first EMAIL-type field.
        String emailField = null;
        if (String.isNotBlank(emailFieldOverride)
                && fieldMap.containsKey(emailFieldOverride.toLowerCase())) {
            emailField = emailFieldOverride;
        } else {
            emailField = findEmailField(fieldMap);
        }
        String nameField = fieldMap.containsKey('name') ? 'Name' : null;

        List<String> selectFields = new List<String>();
        if (nameField != null)  selectFields.add(nameField);
        if (emailField != null) selectFields.add(emailField);

        if (!selectFields.isEmpty()) {
            // Field names come from the describe map and object from describe => trusted identifiers.
            String soql = 'SELECT ' + String.join(selectFields, ', ')
                        + ' FROM '  + ctx.objectApiName
                        + ' WHERE Id = :recordId LIMIT 1';
            SObject row = Database.query(soql);
            if (nameField != null)  ctx.recipientName  = String.valueOf(row.get(nameField));
            if (emailField != null) ctx.recipientEmail = String.valueOf(row.get(emailField));
        }

        // Org-wide "From" addresses (verified only).
        ctx.orgWideAddresses = new List<Option>();
        for (OrgWideEmailAddress owa : [
                SELECT Id, Address, DisplayName
                FROM OrgWideEmailAddress
                ORDER BY DisplayName ]) {
            ctx.orgWideAddresses.add(
                new Option(owa.DisplayName + ' <' + owa.Address + '>', owa.Id));
        }
        return ctx;
    }

    private static String findEmailField(Map<String, Schema.SObjectField> fieldMap) {
        // Prefer a standard "Email" field if present, else first EMAIL-type field.
        if (fieldMap.containsKey('email')) return 'Email';
        for (Schema.SObjectField f : fieldMap.values()) {
            Schema.DescribeFieldResult dfr = f.getDescribe();
            if (dfr.getType() == Schema.DisplayType.EMAIL && dfr.isAccessible()) {
                return dfr.getName();
            }
        }
        return null;
    }

    // ---------------------------------------------------------------------
    // Templates
    // ---------------------------------------------------------------------
    @AuraEnabled(cacheable=true)
    public static List<Option> getEmailTemplates() {
        List<Option> options = new List<Option>();
        for (EmailTemplate t : [
                SELECT Id, Name, FolderName
                FROM EmailTemplate
                WHERE IsActive = true AND TemplateType != 'visualforce'
                ORDER BY Name
                LIMIT 500 ]) {
            String label = String.isBlank(t.FolderName)
                ? t.Name
                : t.Name + '  (' + t.FolderName + ')';
            options.add(new Option(label, t.Id));
        }
        return options;
    }

    /**
     * Renders a stored template, resolving merge fields against the recipient.
     * whoId must be a Contact/Lead/User for recipient merge fields to resolve.
     */
    @AuraEnabled
    public static RenderedTemplate renderTemplate(Id templateId, Id whoId, Id whatId) {
        try {
            Messaging.SingleEmailMessage rendered =
                Messaging.renderStoredEmailTemplate(templateId, whoId, whatId);
            RenderedTemplate r = new RenderedTemplate();
            r.subject  = rendered.getSubject();
            r.htmlBody = rendered.getHtmlBody();
            return r;
        } catch (Exception e) {
            throw new AuraHandledException('Could not render template: ' + e.getMessage());
        }
    }

    // ---------------------------------------------------------------------
    // Send
    // ---------------------------------------------------------------------
    @AuraEnabled
    public static void sendEmail(EmailRequest req) {
		System.debug('REQ IN:: ' + JSON.serialize(req));  // temporary
        if (req == null || req.toAddresses == null || req.toAddresses.isEmpty()) {
            throw new AuraHandledException('At least one "To" recipient is required.');
        }

		// Render once, so the sent email matches the preview exactly.
        // Messaging.SingleEmailMessage rendered =
        //     Messaging.renderStoredEmailTemplate(templateId, contactId, null);

        Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();
        msg.setToAddresses(req.toAddresses);
        if (req.ccAddresses  != null && !req.ccAddresses.isEmpty())  msg.setCcAddresses(req.ccAddresses);
        if (req.bccAddresses != null && !req.bccAddresses.isEmpty()) msg.setBccAddresses(req.bccAddresses);
        msg.setSubject(String.isBlank(req.subject) ? '' : req.subject);
        msg.setHtmlBody(String.isBlank(req.htmlBody) ? '' : req.htmlBody);
        msg.setSaveAsActivity(false); // default; enabled below only when valid

        if (String.isNotBlank(req.orgWideEmailAddressId)) {
            msg.setOrgWideEmailAddressId(req.orgWideEmailAddressId);
        }

        // Log the email as an activity on a Contact/Lead (Users cannot save-as-activity).
        if (String.isNotBlank(req.targetObjectId)) {
            String targetObj = ((Id) req.targetObjectId).getSObjectType().getDescribe().getName();
            msg.setTargetObjectId(req.targetObjectId);
            // We explicitly control the To addresses, so don't treat target as recipient.
            msg.setTreatTargetObjectAsRecipient(false);
            if (req.saveAsActivity == true && targetObj != 'User') {
                msg.setSaveAsActivity(true);
            }
            // Related To — the platform only honors whatId when the target is a Contact.
            if (req.whatId != null && targetObj == 'Contact') {
                msg.setWhatId(req.whatId);
            }
        }

        if (req.contentDocumentIds != null && !req.contentDocumentIds.isEmpty()) {
            List<Id> versionIds = new List<Id>();
            for (ContentVersion cv : [
                    SELECT Id
                    FROM ContentVersion
                    WHERE ContentDocumentId IN :req.contentDocumentIds
                    AND IsLatest = true ]) {
                versionIds.add(cv.Id);
            }
            if (!versionIds.isEmpty()) {
                msg.setEntityAttachments(versionIds);
            }
        }

        // allOrNone = false so we can inspect and surface a clean error.
        List<Messaging.SendEmailResult> results =
            Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ msg }, false);

        if (results != null && !results.isEmpty() && !results[0].isSuccess()) {
            String err = results[0].getErrors().isEmpty()
                ? 'Unknown send failure.'
                : results[0].getErrors()[0].getMessage();
            throw new AuraHandledException('Email failed to send: ' + err);
        }

        // WhatId branch: non-person row (no Contact target). setWhatId can't relate
        // the email here, so log a completed Task on the Related To record instead.
        if (req.whatId != null && String.isBlank(req.targetObjectId)) {
            try {
                insert buildEmailTask(req);
            } catch (Exception e) {
                throw new AuraHandledException('Email sent, but logging the activity failed: ' + e.getMessage());
            }
        }
    }

    private static Task buildEmailTask(EmailRequest req) {
        String toLine = (req.toAddresses == null) ? '' : String.join(req.toAddresses, ', ');
        String plain = String.isBlank(req.htmlBody)
            ? ''
            : req.htmlBody.replaceAll('<[^>]+>', ' ').replaceAll('\\s+', ' ').trim();

        return new Task(
            WhatId       = req.whatId,
            Subject      = 'Email: ' + (String.isBlank(req.subject) ? '(no subject)' : req.subject),
            Description  = 'To: ' + toLine + '\n\n' + plain,
            Status       = 'Completed',
            Priority     = 'Normal',
            ActivityDate = Date.today(),
            TaskSubtype  = 'Email'
        );
    }

    private static String findNameField(Map<String, Schema.SObjectField> fieldMap) {
        for (Schema.SObjectField f : fieldMap.values()) {
            Schema.DescribeFieldResult dfr = f.getDescribe();
            if (dfr.isNameField()) return dfr.getName();  // 'Name', 'CaseNumber', 'OrderNumber', ...
        }
        return 'Name';
    }
}
```

{% endcode %}

</details>

### Lightning Web Component — sendEmailCustom

<details>

<summary>lwc/sendEmailCustom/sendEmailCustom.js</summary>

{% code overflow="wrap" %}

```
import { LightningElement, api, wire } from 'lwc';
import { CloseActionScreenEvent } from 'lightning/actions';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

import getEmailContext from '@salesforce/apex/SendEmailController.getEmailContext';
import getEmailTemplates from '@salesforce/apex/SendEmailController.getEmailTemplates';
import renderTemplate from '@salesforce/apex/SendEmailController.renderTemplate';
import sendEmail from '@salesforce/apex/SendEmailController.sendEmail';
import { MessageContext, publish } from 'lightning/messageService';
import callbackMessage from '@salesforce/messageChannel/valorxwave__callbackMessageChannel__c';

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export default class SendEmail extends LightningElement {
    // recordId of the row the action was invoked on.
    @api recordId;
    // Optional: override the email field API name (configurable in the grid action).
    @api emailField;
    @api relatedToObjects; // optional CSV override of the object list

    @wire(MessageContext) messageContext;

    relatedObjectType = '';
    whatId = '';

    invokeValorxCallBack(isSuccess = false) {
        const payload = {
            status: isSuccess ? 'SUCCESS' : 'ERROR',
        };
        publish(this.messageContext, callbackMessage, payload);
    }

    // From
    fromOptions = [];
    selectedFromAddress = '';

    // Templates
    templateOptions = [];
    selectedTemplate = '';

    // Recipients (pill item shape: { label, name })
    toRecipients = [];
    ccRecipients = [];
    bccRecipients = [];
    toInput = '';
    ccInput = '';
    bccInput = '';

    showCc = false;
    showBcc = false;
    nameField = 'Name';
    subject = '';
    htmlBody = '';
    uploadedFiles = [];

    isMessageable = false;
    targetObjectId;
    isLoading = true;
    isSending = false;

    // ---------------------------------------------------------------- lifecycle
    connectedCallback() {
        this.init();
    }

    async init() {
        try {
            const [ctx, templates] = await Promise.all([
                getEmailContext({ recordId: this.recordId, emailFieldOverride: this.emailField }),
                getEmailTemplates()
            ]);

            this.fromOptions = (ctx.orgWideAddresses || []).map((o) => ({
                label: o.label,
                value: o.value
            }));
            if (this.fromOptions.length) {
                this.selectedFromAddress = this.fromOptions[0].value;
            }

            const objType = ctx.objectApiName;
            const isPerson = objType === 'Contact' || objType === 'Lead';

            if (isPerson) {
                // WhoId branch: the row is the recipient; auto-fill To.
                this.targetObjectId = this.recordId;
                if (ctx.recipientEmail && EMAIL_REGEX.test(ctx.recipientEmail)) {
                    this.toRecipients = [{ label: ctx.recipientEmail, name: ctx.recipientEmail }];
                }
                // Related To stays hidden for Contact/Lead.
            } else {
                // WhatId branch: the row is the Related To record.
                this.whatId = this.recordId;
                this.relatedObjectType = objType;
                this.nameField = ctx.nameField || 'Name';   // add this line
            }
            console.log('What Id:'+this.whatId + this.recordId);
            console.log('relatedObjectType: '+this.relatedObjectType + objType);
            this.isMessageable = isPerson;

            this.templateOptions = [
                { label: 'None', value: '' },
                ...(templates || []).map((t) => ({ label: t.label, value: t.value }))
            ];
        } catch (error) {
            this.showError('Could not load the email composer', error);
        } finally {
            this.isLoading = false;
        }
    }

    get matchingInfo() {
        return { primaryField: { fieldPath: this.nameField } };
    }

    // ---------------------------------------------------------------- getters
    get hasToRecipients()  { return this.toRecipients.length > 0; }
    get hasCcRecipients()  { return this.ccRecipients.length > 0; }
    get hasBccRecipients() { return this.bccRecipients.length > 0; }
    get hasAttachments()   { return this.uploadedFiles.length > 0; }
    get sendDisabled()     { return this.isSending || this.toRecipients.length === 0; }

    // ---------------------------------------------------------------- From / template
    handleFromChange(event) {
        this.selectedFromAddress = event.detail.value;
    }

    async handleTemplateChange(event) {
        this.selectedTemplate = event.detail.value;
        if (!this.selectedTemplate) return;

        this.isLoading = true;
        try {
            const rendered = await renderTemplate({
                templateId: this.selectedTemplate,
                whoId: this.isMessageable ? this.recordId : null,
                whatId: null
            });
            if (rendered.subject) this.subject = rendered.subject;
            if (rendered.htmlBody) this.htmlBody = rendered.htmlBody;
        } catch (error) {
            this.showError('Could not apply template', error);
        } finally {
            this.isLoading = false;
        }
    }

    handleRelatedTypeChange(event) {
        this.relatedObjectType = event.detail.value;
        this.whatId = ''; // reset the record when the object type changes
        const picker = this.template.querySelector('lightning-record-picker');
        if (picker) picker.clearSelection?.();
    }

    handleRelatedToChange(event) {
        this.whatId =  this.recordId || '';
    }

    // ---------------------------------------------------------------- recipient handling
    handleToInput(e)  { this.toInput = e.detail.value; }
    handleCcInput(e)  { this.ccInput = e.detail.value; }
    handleBccInput(e) { this.bccInput = e.detail.value; }

    handleToKeyDown(e)  { if (this.isCommit(e)) this.commitTo(); }
    handleCcKeyDown(e)  { if (this.isCommit(e)) this.commitCc(); }
    handleBccKeyDown(e) { if (this.isCommit(e)) this.commitBcc(); }

    handleToBlur()  { this.commitTo(); }
    handleCcBlur()  { this.commitCc(); }
    handleBccBlur() { this.commitBcc(); }

    isCommit(e) {
        return e.key === 'Enter' || e.key === ',' || e.key === ';';
    }

    commitTo()  { this.toRecipients  = this.addAddresses(this.toRecipients, this.toInput);  this.toInput = ''; }
    commitCc()  { this.ccRecipients  = this.addAddresses(this.ccRecipients, this.ccInput);  this.ccInput = ''; }
    commitBcc() { this.bccRecipients = this.addAddresses(this.bccRecipients, this.bccInput); this.bccInput = ''; }

    addAddresses(current, raw) {
        if (!raw) return current;
        const existing = new Set(current.map((r) => r.name.toLowerCase()));
        const result = [...current];
        raw.split(/[,;\s]+/).forEach((token) => {
            const addr = token.trim();
            if (addr && EMAIL_REGEX.test(addr) && !existing.has(addr.toLowerCase())) {
                result.push({ label: addr, name: addr });
                existing.add(addr.toLowerCase());
            } else if (addr && !EMAIL_REGEX.test(addr)) {
                this.showToast('Invalid address', `"${addr}" is not a valid email.`, 'warning');
            }
        });
        return result;
    }

    handleToRemove(e)  { this.toRecipients  = this.removeAt(this.toRecipients, e.detail.index); }
    handleCcRemove(e)  { this.ccRecipients  = this.removeAt(this.ccRecipients, e.detail.index); }
    handleBccRemove(e) { this.bccRecipients = this.removeAt(this.bccRecipients, e.detail.index); }

    removeAt(list, index) {
        const copy = [...list];
        copy.splice(index, 1);
        return copy;
    }

    toggleCc()  { this.showCc = !this.showCc; }
    toggleBcc() { this.showBcc = !this.showBcc; }

    // ---------------------------------------------------------------- body / subject / files
    handleSubjectChange(e) { this.subject = e.detail.value; }
    handleBodyChange(e)    { this.htmlBody = e.target.value; }

    handleUploadFinished(e) {
        const files = e.detail.files || [];
        this.uploadedFiles = [
            ...this.uploadedFiles,
            ...files.map((f) => ({ documentId: f.documentId, name: f.name }))
        ];
    }

    // ---------------------------------------------------------------- send
    async handleSend() {
        // Flush any address still sitting in an input box.
        this.commitTo(); this.commitCc(); this.commitBcc();

        if (this.toRecipients.length === 0) {
            this.showToast('Missing recipient', 'Add at least one "To" address.', 'warning');
            return;
        }

        this.isSending = true;
        try {
            console.log('To Email:: '+this.toRecipients.map((r) => r.name));
            const payload = {
                recordId: this.recordId,
                toAddresses:  this.toRecipients.map((r) => r.name),
                ccAddresses:  this.ccRecipients.map((r) => r.name),
                bccAddresses: this.bccRecipients.map((r) => r.name),
                subject: this.subject,
                htmlBody: this.htmlBody,
                orgWideEmailAddressId: this.selectedFromAddress,
                saveAsActivity: this.isMessageable,
                targetObjectId: this.targetObjectId,
                contentDocumentIds: this.uploadedFiles.map((f) => f.documentId),
                whatId: this.whatId || null
            };
            console.log('typeof arg:', typeof payload);
            await sendEmail({ req: payload });
            this.showToast('Sent', 'Your email was sent.', 'success');
            this.invokeValorxCallBack(true);
            this.closeComposer();
        } catch (error) {
            this.showError('Could not send email', error);
        } finally {
            this.isSending = false;
        }
    }

    handleCancel() {
        this.invokeValorxCallBack(false);
        this.closeComposer();
    }

    // ---------------------------------------------------------------- close (works in both contexts)
    closeComposer() {
        // Native quick-action context:
        this.dispatchEvent(new CloseActionScreenEvent());
        // Valorx grid action context: bubble a custom close for the host container.
        this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
    }

    // ---------------------------------------------------------------- toasts
    showToast(title, message, variant) {
        this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
    }

    showError(title, error) {
        const message =
            error?.body?.message ||
            error?.message ||
            'An unexpected error occurred.';
        this.showToast(title, message, 'error');
    }
}
```

{% endcode %}

</details>

<details>

<summary>lwc/sendEmailCustom/sendEmailCustom.html</summary>

{% code overflow="wrap" %}

```
<template>
    <section role="dialog" tabindex="-1" aria-modal="true" aria-labelledby="modal-heading"
             class="slds-modal slds-fade-in-open slds-modal_medium">
        <div class="slds-modal__container">

            <!-- Header -->
            <header class="slds-modal__header">
                <lightning-button-icon icon-name="utility:close" variant="bare-inverse"
                    size="large" alternative-text="Close" title="Close"
                    class="slds-modal__close" onclick={handleCancel}>
                </lightning-button-icon>
                <h2 id="modal-heading" class="slds-modal__title slds-hyphenate">Email</h2>
            </header>

            <!-- Body -->
            <div class="slds-modal__content slds-p-around_medium" style="min-height:30rem;">

                <template lwc:if={isLoading}>
                    <lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
                </template>

                <!-- From -->
                <lightning-combobox
                    label="From"
                    placeholder="Select a From address"
                    value={selectedFromAddress}
                    options={fromOptions}
                    onchange={handleFromChange}
                    class="slds-m-bottom_x-small">
                </lightning-combobox>

                <!-- To -->
                <div class="slds-form-element slds-m-bottom_x-small">
                    <div class="slds-grid slds-grid_align-spread">
                        <label class="slds-form-element__label">To</label>
                        <div class="cc-bcc-toggles">
                            <a onclick={toggleCc} class="slds-text-link_reset slds-m-right_small">Cc</a>
                            <a onclick={toggleBcc} class="slds-text-link_reset">Bcc</a>
                        </div>
                    </div>
                    <template lwc:if={hasToRecipients}>
                        <lightning-pill-container items={toRecipients}
                            onitemremove={handleToRemove}></lightning-pill-container>
                    </template>
                    <lightning-input variant="label-hidden" type="text"
                        placeholder="Type an email address and press Enter"
                        value={toInput}
                        onchange={handleToInput}
                        onkeydown={handleToKeyDown}
                        onblur={handleToBlur}>
                    </lightning-input>
                </div>

                <!-- Cc -->
                <template lwc:if={showCc}>
                    <div class="slds-form-element slds-m-bottom_x-small">
                        <label class="slds-form-element__label">Cc</label>
                        <template lwc:if={hasCcRecipients}>
                            <lightning-pill-container items={ccRecipients}
                                onitemremove={handleCcRemove}></lightning-pill-container>
                        </template>
                        <lightning-input variant="label-hidden" type="text"
                            placeholder="Add Cc" value={ccInput}
                            onchange={handleCcInput}
                            onkeydown={handleCcKeyDown}
                            onblur={handleCcBlur}>
                        </lightning-input>
                    </div>
                </template>

                <!-- Bcc -->
                <template lwc:if={showBcc}>
                    <div class="slds-form-element slds-m-bottom_x-small">
                        <label class="slds-form-element__label">Bcc</label>
                        <template lwc:if={hasBccRecipients}>
                            <lightning-pill-container items={bccRecipients}
                                onitemremove={handleBccRemove}></lightning-pill-container>
                        </template>
                        <lightning-input variant="label-hidden" type="text"
                            placeholder="Add Bcc" value={bccInput}
                            onchange={handleBccInput}
                            onkeydown={handleBccKeyDown}
                            onblur={handleBccBlur}>
                        </lightning-input>
                    </div>
                </template>

                <!-- Subject -->
                <lightning-input type="text" label="Subject" value={subject}
                    onchange={handleSubjectChange} class="slds-m-bottom_x-small">
                </lightning-input>

                <!-- Insert Template -->
                <lightning-combobox
                    label="Insert a template"
                    placeholder="None"
                    value={selectedTemplate}
                    options={templateOptions}
                    onchange={handleTemplateChange}
                    class="slds-m-bottom_x-small">
                </lightning-combobox>

                <!-- Body -->
                <lightning-input-rich-text value={htmlBody}
                    onchange={handleBodyChange}
                    placeholder="Write your message..."
                    class="email-body">
                </lightning-input-rich-text>

                <!-- Related To -->
                <div class="slds-form-element slds-m-bottom_x-small slds-m-top_small">
                    <!-- Related To — shown only for non-person rows (WhatId branch) -->
                    <template lwc:if={relatedObjectType}>
                        <div class="slds-grid slds-grid_vertical-align-center">
                            <span class="slds-text-title slds-m-right_x-small">Related To</span>
                            <div class="related-to-record">
                                <lightning-record-picker
                                    label="Related To"
                                    variant="label-hidden"
                                    placeholder="Search..."
                                    object-api-name={relatedObjectType}
                                    matching-info={matchingInfo}
                                    value={whatId}
                                    onchange={handleRelatedToChange}>
                                </lightning-record-picker>
                            </div>
                        </div>
                    </template>
                    <template lwc:else>
                        <span></span> <!-- spacer keeps actions right-aligned -->
                    </template>
                </div>

                <!-- Attachments -->
                <div class="slds-m-top_small">
                    <lightning-file-upload
                        label="Attach files"
                        name="emailAttachments"
                        record-id={recordId}
                        multiple
                        onuploadfinished={handleUploadFinished}>
                    </lightning-file-upload>

                    <template lwc:if={hasAttachments}>
                        <ul class="slds-list_dotted slds-m-top_x-small">
                            <template for:each={uploadedFiles} for:item="file">
                                <li key={file.documentId} class="slds-item">
                                    <lightning-icon icon-name="doctype:attachment" size="x-small"
                                        class="slds-m-right_xx-small"></lightning-icon>
                                    {file.name}
                                </li>
                            </template>
                        </ul>
                    </template>
                </div>
                
            </div>
            <!-- Footer -->
            <footer class="slds-modal__footer">
                <lightning-button label="Cancel" onclick={handleCancel}
                    class="slds-m-right_x-small"></lightning-button>
                <lightning-button variant="brand" label="Send" onclick={handleSend}
                    disabled={sendDisabled}></lightning-button>
            </footer>
        </div>
    </section>
    <div class="slds-backdrop slds-backdrop_open"></div>
</template>
```

{% endcode %}

</details>

<details>

<summary>lwc/sendEmailCustom/sendEmailCustom.css</summary>

{% code overflow="wrap" %}

```
.email-body {
    --slds-c-textarea-sizing-min-height: 15rem;
}

.email-body .ql-editor,
.email-body lightning-input-rich-text {
    min-height: 15rem;
}

.cc-bcc-toggles a {
    cursor: pointer;
    font-size: 0.8125rem;
    color: var(--slds-g-link-color, #0176d3);
}

.slds-modal__content {
    max-height: calc(80vh - 6rem);
    overflow-y: auto;
}
```

{% endcode %}

</details>

<details>

<summary>lwc/sendEmailCustom/sendEmailCustom.js-meta.xml</summary>

{% code overflow="wrap" %}

```
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<isExposed>true</isExposed>
<apiVersion>67.0</apiVersion>
<masterLabel>Send Email Custom</masterLabel>
<targets>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
<target>lightning__RecordPage</target>
</targets>
</LightningComponentBundle>
```

{% endcode %}

</details>

### Deployment Manifest

<details>

<summary>package.xml</summary>

```
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
 <types>
  <members>SendEmailController</members>
  <name>ApexClass</name>
 </types>
 <types>
  <members>sendEmailCustom</members>
  <name>LightningComponentBundle</name>
 </types>
<version>67.0</version>
</Package>
```

</details>
