LWCの確認画面からApexでメールを送信する実装
宛先を選んですぐ送ると、誤送信に気づけません。LWCで宛先と本文を確認してから、Apexのメール送信に進む2ステップの画面を説明します。
シナリオを整理する
取引先責任者の詳細画面から、宛先・件名・本文を確認したうえでメールを送るクイックアクションです。選んだらすぐ送るのではなく、確認をはさむ2ステップ構成にすると、誤送信を防げます。Step1で送信先を選び、Step2で内容を確認してから送信します。
画面の構成
| ステップ | 画面の内容 |
|---|---|
| Step1 | lightning-datatableで送信対象の取引先責任者をチェックボックスで選ぶ |
| Step2 | Apexが組み立てた宛先・件名・本文をプレビューし、sendボタンで送信する |
現在地はlightning-progress-indicatorで示します。
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件で、契約があればサポートへ依頼して引き上げられます |
動作を確認する
取引先責任者レコードでアクションを実行する
Step1の一覧が開きます。
送信先をチェックしNextを押す
選んだ取引先責任者をもとに、Apexが宛先・件名・本文を組み立てます。
Step2で内容を確認しsendを押す
表示されている宛先・件名・本文の通りにメールが送信されます。
ここで間違えやすい
| 間違い | 何が起きるか |
|---|---|
| ラベルの誤字(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との連携まで承ります。状況を伺ったうえで、進め方をご提案します。