LWC

LWCの確認画面からApexでメールを送信する実装

宛先を選んですぐ送ると、誤送信に気づけません。LWCで宛先と本文を確認してから、Apexのメール送信に進む2ステップの画面を説明します。

2023.07.11

シナリオを整理する

取引先責任者の詳細画面から、宛先・件名・本文を確認したうえでメールを送るクイックアクションです。選んだらすぐ送るのではなく、確認をはさむ2ステップ構成にすると、誤送信を防げます。Step1で送信先を選び、Step2で内容を確認してから送信します。

取引先責任者ContactAの詳細画面。所有者や名前などの項目が縦に並んでいる
クイックアクションを実行する取引先責任者の詳細画面です。ここから右上のメニューを開いて、メール送信のアクションを呼び出します。
画面右上のメニューが開き、一番下のLWCメール送信が赤枠で示されている
右上のメニューを開いたところです。赤枠で囲んだLWCメール送信が、今回追加したクイックアクションにあたります。

画面の構成

ステップ画面の内容
Step1lightning-datatableで送信対象の取引先責任者をチェックボックスで選ぶ
Step2Apexが組み立てた宛先・件名・本文をプレビューし、sendボタンで送信する

現在地はlightning-progress-indicatorで示します。

Step1のcheck画面。Name列にContactAが1件だけ表示された一覧
Step1の画面です。上部の進行状況インジケーターと、送信先を選ぶチェックボックス付きの一覧をご覧ください。
Step1でContactAのチェックを入れた状態。右下にCancealとNextのボタンが並ぶ
送信先を選ぶとNextが押せるようになります。Nextの左にあるボタンのラベルが、当時はCancealと誤字だった点にご注意ください。
Step2の確認画面。本文にLWCテストメール送信しますと表示され、sendボタンが赤枠
Step2の確認画面です。宛先と本文を確認したうえで、赤枠のsendボタンを押すとメールが送信されます。

LWC本体

sendEmailContactScreenAction.html

<template>
    <div style="margin-top: 20px">
        <lightning-progress-indicator current-step={currentStep}>
            <lightning-progress-step label="check" value="step1" onclick={handleClickStep}></lightning-progress-step>
            <lightning-progress-step label="Email Review" value="step2" onclick={handleClickStep}></lightning-progress-step>
        </lightning-progress-indicator>
    </div>
    <template if:true={isStep1}>
        <lightning-quick-action-panel header="check">
            <div class="slds-scrollable" style="height: 400px">
                <lightning-datatable key-field="id" data={rows} columns={columns} show-checkbox-column onrowselection={handleRowSelection}>
                </lightning-datatable>
            </div>
            <div slot="footer">
                <lightning-button variant="neutral" label="Cancel" onclick={handleClickCancel} class="slds-m-left_x-small"></lightning-button>
                <lightning-button variant="brand" label="Next" onclick={handleClickNext} disabled={isNotSelected} class="slds-m-left_x-small"></lightning-button>
            </div>
        </lightning-quick-action-panel>
    </template>
    <template if:true={isStep2}>
        <lightning-quick-action-panel header="Email Review">
            <div>Sender:{email.fromAddress}</div>
            <div>To:{email.toAddress}</div>
            <div>Subject:{email.subject}</div>
            <div class="slds-border_top" style="margin-top: 10px"></div>
            <div class="slds-scrollable" style="height: 290px">
                <lightning-formatted-rich-text value={email.bodyHtml}></lightning-formatted-rich-text>
            </div>
            <div slot="footer">
                <lightning-button variant="neutral" label="return" onclick={handleClickPrev} class="slds-m-left_x-small"></lightning-button>
                <lightning-button variant="brand" label="send" onclick={handleClickSend} class="slds-m-left_x-small"></lightning-button>
            </div>
            <lightning-spinner if:true={sending} alternative-text="sending..." variant="brand"></lightning-spinner>
        </lightning-quick-action-panel>
    </template>
</template>

Step1で対象を選び、Step2で内容を確認してから送信します。ラベルの誤字(Canceal)をCancelへ直しています。

sendEmailContactScreenAction.js

import { api, track, wire, LightningElement } from "lwc";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import { CloseActionScreenEvent } from "lightning/actions";
import { CurrentPageReference } from "lightning/navigation";
import { RefreshEvent } from "lightning/refresh";
import getRecords from "@salesforce/apex/SendEmailContactController.getRecords";
import getEmail from "@salesforce/apex/SendEmailContactController.getEmail";
import sendEmail from "@salesforce/apex/SendEmailContactController.sendEmail";
export default class SendEmailContactScreenAction extends LightningElement {
    @api recordId;
    @track currentStep = "step1";
    columns = [
        {
            label: "Name",
            fieldName: "Url",
            type: "url",
            typeAttributes: { label: { fieldName: "Name" }, target: "_blank" },
        },
    ];
    @track rows = [];
    selectedRows = [];
    @track email;
    @track sending = false;

    @wire(CurrentPageReference)
    async getStateParameters(currentPageReference) {
        this.recordId = currentPageReference.state.recordId || null;
        const rows = await getRecords({ recordId: this.recordId }).catch((e) => {
            console.error(e);
            this.dispatchEvent(
                new ShowToastEvent({ title: "Error occurred.", message: e.body.message, variant: "error" })
            );
        });
        this.rows = rows.map((x) => ({ Url: `/lightning/r/${x.Id}/view`, ...x }));
    }
    handleClickCancel() {
        this.dispatchEvent(new CloseActionScreenEvent());
    }
    async handleClickNext() {
        if (this.selectedRows.length === 0) {
            this.dispatchEvent(new ShowToastEvent({ title: "Please check.", variant: "error" }));
            return;
        }
        const email = await getEmail({
            contactIds: Array.from(this.selectedRows.map((x) => x.Id)),
        }).catch((e) => {
            console.error(e);
            this.dispatchEvent(
                new ShowToastEvent({ title: "Error occurred.", message: e.body.message, variant: "error" })
            );
        });
        this.email = JSON.parse(email);
        this.currentStep = "step2";
    }
    handleRowSelection(event) {
        this.selectedRows = Array.from(event.detail.selectedRows);
    }
    handleClickPrev() {
        this.currentStep = "step1";
        this.selectedRows = [];
    }
    async handleClickSend() {
        this.sending = true;
        await sendEmail({
            contactIds: Array.from(this.selectedRows.map((x) => x.Id)),
        }).catch((e) => {
            console.error(e);
            this.dispatchEvent(
                new ShowToastEvent({ title: "Error occurred.", message: e.body.message, variant: "error" })
            );
        });
        this.dispatchEvent(new RefreshEvent());
        this.sending = false;
        this.dispatchEvent(new CloseActionScreenEvent());
    }
    handleClickStep(event) {
        if (event.target.value === "step1") {
            this.handleClickPrev();
        } else if (event.target.value === "step2") {
            this.handleClickNext();
        }
    }
    get isStep1() {
        return this.currentStep === "step1";
    }
    get isStep2() {
        return this.currentStep === "step2";
    }
}

Auraのグローバル$Aをevalで呼んでいた画面リロードを、lightning/refreshのRefreshEventへ書き換えています。

以前の書き方ではeval("$A.get('e.force:refreshView').fire()")で画面を更新していました。これはLWCの中でAuraのグローバル関数を直接呼ぶ書き方で、公式に案内されている方法ではありません。現在はlightning/refreshモジュールのRefreshEventを使うのが、コンテナへ更新を伝える公式な方法です。

sendEmailContactScreenAction.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>67.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordAction</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordAction">
            <actionType>ScreenAction</actionType>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

apiVersionを57.0から67.0(Summer '26)へ上げています。

Apexで本文を組み立てて送る

SendEmailContactController.cls

public with sharing class SendEmailContactController {
    public class Email {
        public String fromAddress;
        public String toAddress;
        public String subject;
        public String bodyHtml;
        public String bodyText;
        public Email(List<Id> contactIds) {
            List<Contact> contacts = [SELECT Id, Email FROM Contact WHERE Id IN :contactIds];
            Contact contact = contacts[0];
            this.fromAddress = UserInfo.getUserEmail();
            this.toAddress = contact.Email;
            this.subject = 'LWCテストメール送信';
            String body = String.join(
                new List<String>{ '', 'LWCテストメール送信します。', '' },
                '\n'
            );
            this.bodyHtml = body.replace('\n', '<br/>');
            this.bodyText = body.stripHtmlTags();
        }
    }
    @AuraEnabled(cacheable=false)
    public static List<Contact> getRecords(Id recordId) {
        return [SELECT Id, Name, Email FROM Contact WHERE Id = :recordId];
    }
    @AuraEnabled(cacheable=false)
    public static Object getEmail(List<Id> contactIds) {
        Email email = new Email(contactIds);
        return JSON.serialize(email);
    }
    @AuraEnabled(cacheable=false)
    public static Boolean sendEmail(List<Id> contactIds) {
        Email email = new Email(contactIds);
        Messaging.SingleEmailMessage singleEmail = new Messaging.SingleEmailMessage();
        singleEmail.setToAddresses(new String[] { email.toAddress });
        singleEmail.setSubject(email.subject);
        singleEmail.setHtmlBody(email.bodyHtml);
        singleEmail.setPlainTextBody(email.bodyText);
        singleEmail.setBccSender(true);
        Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ singleEmail });
        return true;
    }
}

SOQLはすべてバインド変数で、ループの中では発行していません。

⚠️ Step2の画面に表示しているSenderは、Apexが組み立てた表示用の文字列であって、実際の送信元アドレスを固定するものではありません。SingleEmailMessageに送信元を設定しない場合、送信元は実行ユーザーの既定のメールアドレスになります。送信元を組織のアドレスへ固定したいときは、検証済みの組織のメールアドレス(OrgWideEmailAddress)のIdを指定します。

singleEmail.setOrgWideEmailAddressId(orgWideEmailAddressId);

送信元を組織の共有アドレスへ固定したいときに、sendEmailメソッドの中へ1行加えます。

メールの上限を知っておく

上限内容
1トランザクションあたりの送信回数Messaging.sendEmailメソッドを呼び出せるのは、1トランザクションあたり10回までです
1通あたりの宛先数toAddresses・ccAddresses・bccAddressesの合計で最大150件、各欄は最大4,000バイトです
1日あたりの送信数SingleEmailMessageで送れる宛先は、通常の組織で1日5,000件までです。試用組織は既定15件で、契約があればサポートへ依頼して引き上げられます

動作を確認する

  1. 取引先責任者レコードでアクションを実行する

    Step1の一覧が開きます。

  2. 送信先をチェックしNextを押す

    選んだ取引先責任者をもとに、Apexが宛先・件名・本文を組み立てます。

  3. Step2で内容を確認しsendを押す

    表示されている宛先・件名・本文の通りにメールが送信されます。

受信トレイに届いた1通のメール。件名はLWCテストメール送信
送信後に実際に届いたメールです。件名と本文が、Apexで組み立てた内容どおりになっているか確認します。

ここで間違えやすい

間違い何が起きるか
ラベルの誤字(Canceal)をそのまま使う誤字入りのボタンがそのまま画面に出ます
Step2の表示だけを見て送信元を判断する実際の送信元は表示と一致するとは限りません。固定したいときはsetOrgWideEmailAddressIdを使います
宛先の合計が150件を超える設計にするMessaging.sendEmailが失敗します。to・cc・bccの合計で数えます
1日5,000件の上限を意識せず一括送信を設計する上限に達した時点から送れなくなります

確認した環境

  • 2026年9月 / Salesforce Summer '26(API バージョン 67.0)時点の公式ドキュメントで、Messaging.SingleEmailMessageの宛先件数の上限、1トランザクションあたりの送信回数の上限、1日あたりの送信件数の上限、setOrgWideEmailAddressIdの使い方を確認しています
  • コードは API バージョン 67.0 で書いています

まとめ

  • 選んだらすぐ送らず、確認をはさむ2ステップにすると誤送信を防げます
  • eval("$A.get(...)")によるAura経由の画面更新は、lightning/refreshのRefreshEventへ書き換えます
  • 表示用の送信元と実際の送信元は別物です。固定したいときはsetOrgWideEmailAddressIdを使います
  • sendEmailの呼び出しは1トランザクション10回まで、1通の宛先は最大150件、1日の宛先は通常5,000件までです
  • apiVersionは57.0から67.0(Summer '26)へ上げています

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。