FlowでSchedule Sendを実装してみた

# 背景
Gamilと比べて、SalesforceのデフォルトのEmail Composerには２大欠点があります。
1. 予定時刻を設定する送信予約機能がありません (Schedule Send)
1. 下書き保存がありません（Save as Draft)

`送信予約機能（Schedule Send）`がない弊害は特定の相手とメッセージラリーの応酬になり、かえってそのほかの相手とのやりとりのタイミングに支障をきたす可能性もあります。また、業務時間外の送信や相手の国の営業時間帯に合わせた送信もできないことになります。

また`下書き保存（Save as Draft）`がないので、定型文以外はユーザーがメールを直書きするのを怖がったり、別途清書してコピペするという効率の悪い工程ができたり、Salesforce上でいろいろ練って書いている最中のテキストがすっ飛んだり怨嗟の念が飛び交うことも発生します。

# 解決策
`Schedule Send`も`Save as Draft`も機能として標準Composerにないだけで、技術的には可能です。解法は２つあり、素直に課金サービスを利用するか、開発するかです。
## 課金サービス
### Salesforce Inbox を購入（有償）
:::note info
Salesforce内で購入可能。有償でSalesforce Inbox（もともとRelateIQ; 2014年に買収）はありますが、これはまたChromeなどのExtensionで利用します。
[Salesforce Inbox](https://help.salesforce.com/s/articleView?id=sf.inbox.htm&type=5)
[Extension](https://chromewebstore.google.com/detail/salesforce/jjghhkepijgakdammjldcbnjehfkfmha)
:::
Extensionは機能制限付きで、Inboxランセンスを付与されているユーザーのみそのほかのProductivity Featuresがアンロックになります。有償のサービスがあるとはいえ、ユーザーごとのライセンスなので全ユーザーに現在のメーラーで標準装備のものを課金するのには賛同できないし、スケールした際は無視できないコストになります。

## 開発

### FlowとApexで実装（無償）
一方で、Salesforce機能としては成立しているので２大欠点をカバーする機能の実現は可能です。その際、最低限必要な知識としてEmailMessageとFlowが必要です。Apexでは定型分の検索と選択、添付ファイルの付与・削除等よりカスタムされた設定がスムーズになります。

## 概念
実装にあたって全体像とEmailMessageの概念を簡略化して説明します。
まず、EmailMessageは送信した記録として記録されますが、送信とともに作成されるため、これ自体が送信の実態と勘違しがちですが、EmailMessageはただの記録で、実際の送信は「SendEmailアクション」が行なっています。実際にEmailMessageには`CreatedDate`とは別に`MessageDate`があり、通常は一致していますがあえて別にすることもできます。

::: note info
[SingleEmailMessage Methods](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_classes_email_outbound_single.htm)
こちらでSingleEmailMessageの概要が説明されています。

[Email Class (Base Email Methods)](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_classes_email_outbound_base.htm#apex_classes_email_outbound_base)
こちらでsetSaveAsActivityの挙動の記載がありますが、フロー内の標準コンポーネントでそのtrue, falseの設定が可能になっています。Apexで送る場合は、こちらを制御することで自動で関連レコードを作成するかしないかを制御できます。（データの一貫性のためあえてfalseにする必要はないかと思います）
:::

### 標準
![CleanShot 2024-03-16 at 14.43.19@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/c4100ea6-d70c-202e-6b7a-d7a09bb913a1.png)
通常Caseなどのオブジェクトから標準のEmail Composerから送信する場合、Composerで文章を書き、その内容を送信する。送信した記録としてEmailMessageが作成されTaskでActivityとして連携され表示されます。

### フローから発信
![CleanShot 2024-03-16 at 14.45.51@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/45134a32-38b5-8b48-75ef-9401908244fd.png)
一方フローから発信する場合、②のフローを起動してフロー内にあるEmail Composerでメールを下書きし、④,　⑤で発信します。実際はApexが後ろでSingleMessageが走っているので、SaveAsAtivityをtrueにするかfalseにするか選択するだけで、発信後のそれぞれのレコードの関連を自動で行ってくれます。次のSchedule Sendと比較しやすいように④,　⑤を逆にしています。

### フローからShedule Send 発信
![CleanShot 2024-03-16 at 14.50.34@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/78d135c8-28ba-9110-c919-1e099256aa35.png)
ほぼ上記と同じですが、⑤のFlowはAsyncronous flow で、②で設定するScheduled Datetimeを④のEmailMessageに保存して、⑤を起動した際のAsyncronous Pathで0 min delay後に、⑥のEmail Sendを起動します。


### Flowでの実装手順

今Schedule Sendをフローで実装する方法を紹介します。LWCやAuraに組み込んで行う場合、flowを呼び出すか直接Apexのメソッドを呼び出すこともできます。

#### 下準備
EmailMessageに**２つのカスタム項目**を追加（名前は任意）
1) **Scheduled Datetime** （Datetime型）
Scheduled DatetimeはSchedule Sendの概要②のScreen Flowで設定し、⑤のFlow起動のAsynchronous Pathの起点に使用します。
2) **Parent Id** (Text型)
④をDraftで保存する場合、標準のParentId項目での保持を許容しない*ので、別途カスタム項目として作成し⑥で`Log Email on Send = true`によるレコード保存の際に、EmailMessageにあるParentIdを関連付けするために利用します。これを行うことによって、送信されたEmailMessageが起点のCaseと紐づくことになります。
*おそらく紐付けるとActivityに出現するためかと思われます。Sentステータスだと保存は可能ですが送信されていないものがSentと表示されるのは避けた方がいいため。

Flowは２つ。最初は**Email Composer**用のCase起点のFlow, 次はAsynchronous Pathで　Scheduled Datimeに従って**Email Send**を行うFlowです。


![CleanShot 2024-03-16 at 18.57.37@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/f3804a14-0b95-9b1b-4ec9-32c4f3edf293.png)

#### Email Composer Screenの呼び出し
![CleanShot 2024-03-16 at 16.27.00@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/efe2a50a-9931-cba1-da5c-a92502a0ed51.png)

Screenを利用するのでScreen Flowで To, From, Cc, Bcc等はEmailコンポーネントを利用します。宛先にコンタクトレコードを指定したい場合は、Lookupコンポーネントを利用します。

また **HtmlBody Composer** ですが、標準のText Areaではhtmlの装飾ができないので、このコンポーネントは別途作成する必要があります。 特に装飾が必要ない場合はTextAreaでもTextでも可能です。
<details><summary>Aura </summary>


``` html:cmp
<aura:component implements="lightning:availableForFlowScreens">
    <aura:attribute name="label" type="String" access="global"/>
    <aura:attribute name="value" type="String" access="global"/>
    <aura:attribute name="required" type="Boolean" access="global" default="false" />
    <aura:attribute name="placeHolder" type="String" access="global"/>    
    
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    <aura:attribute name="validate" type="Aura.Action" description="custom validation"/>
    <lightning:inputRichText value="{!v.value}"
                             required="{!v.required}"
                             placeholder="{!v.placeHolder}"
                             labelVisible="true"
                             variant = "bottom-toolbar"
                             label="{!v.label}">
        <lightning:insertImageButton />
        </lightning:inputRichText>
</aura:component>
```

```jsvascript:controller.js
({
	doInit : function(component, event, helper) {
        component.set("v.validate", function(){
            let userInput = component.get("v.value");
            if (userInput){
                return {isValud:true};
            } else if (!userInput && component.get("v.required")){
                return {isValid:false, errorMessage: 'A value is required.'};
            } else {
                return {isValid: true};
            }
        });
	}
})
```

```html:design
<design:component label="HtmlBody Composer">
    <design:attribute name="label" label="Label"/>
    <design:attribute name="value" label="Value"/>
    <design:attribute name="required" label="required" />
    <design:attribute name="placeHolder" label="Place Holder"/> 
</design:component>
```

</details>
<details><summary>LWC </summary>


```html:html
<template>
    <lightning-input-rich-text value={value} required={required} placeholder={placeHolder} label={label}
        variant="bottom-toolbar">
    </lightning-input-rich-text>
</template>
```

```js:js
import { LightningElement, api } from 'lwc';

export default class HtmlBodyEmailComposer extends LightningElement {
    @api label;
    @api value;
    @api required = false;
    @api placeHolder;
}
```
</details>

#### EmailMessageの作成
EmailMessageをDraftとして保存します。 **EmailMessageは曲者** でいくつか制約があります。またStatusを`文字列`としての数値で保存します。また編集権限が"0"(New) -> "1" (Read) のみです。Draftは"5"です。

Status
![CleanShot 2024-03-16 at 16.50.22@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/824181b1-9d49-3708-81f0-de810897500d.png)

:::note info
[EmailMessage](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_emailmessage.htm)
EmailMessage は、Email-to-Caseまたは拡張メールを使用している組織でのみ使用可能です。
:::

![CleanShot 2024-03-16 at 16.48.25@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/df7f2adc-b16e-a27e-59cc-030bae9bcfef.png)


#### EmailMessageカスタム項目の設定
Screen FLowのEmail Composer内の値をEmailMessage に充てていきます。
![CleanShot 2024-03-16 at 18.58.59@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/ac613542-70f3-479f-d167-e0be29bdb623.png)

::: note warn
EmailMessageをParentIdにCase.Idを指定するとStatusはSent（3)で可能です。ただしアクテビティにSentとして表示されてしまうため、Draft（5)で保存する必要があります。

ただし、Draftで保存する場合、ParentIdを指定しないで保存する必要があります。なので、Draft（5）としてのEmailMessageには起点となったEmailMessageのParentIdをカスタム項目（ParentId__c)に保存して、のちのSendEmailアクションの際にRelatedToIdで指定してあげることができます。
:::

#### Attachments を扱う （任意）

FileUpload Componentが標準であるので添付ファイルを扱う場合、以下のコンポーネントを利用することもできますし、カスタムのコンポーネントを作成して利用することもできます。ただし、標準のSend Emailは添付ファイルに対応していないので、Send EmailのアクションがApexでの開発になります。（参照: 下段の[Apexでの実装](https://qiita.com/drafts/1767fa33d8ac607c74d8/edit#apex%E3%81%A7%E3%81%AE%E5%AE%9F%E8%A3%85)）

![CleanShot 2024-03-17 at 13.21.39@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/70465c86-f5c2-bb67-9eb2-ef94e8da80fe.png)

[File Upload](https://developer.salesforce.com/docs/component-library/bundle/lightning-file-upload/documentation)

アップロードしたファイルはContentDocumentとしてContentVersionとともに保存されるので、必須のカスタムオブジェクトと同様、ContentDocumentIdsというカスタム項目で一時保存し、Asynchronous PathのFlowへ渡すことが必要です。

また添付したファイルはAttachmentsとしてEmailMessageに関連づけられるのですが、FilePreviewなどで閲覧できるのはContentDocumentsなので、Attachmentsでの紐付けではなくContentDocumentLinkでの紐付けをApex内で行うとスッキリします。手順は、Attachmentsを削除してContentDocumentLinkを添付ファイルごと作成することで可能になります。

ここで最初のFlowは終了です。行ったことはScreen FlowでEmail Composerを呼び出し、Schedule DateTimeを設定、EmailMessageを下書きとして作成です。このEmailMessageを作成しただけではEmail自体送信されません。あくまでもSend Emailアクションが送信を担当します。Send Email Actionは次の別Flowで起動されます。

#### Asyncronous Pathの呼び出し
![CleanShot 2024-03-16 at 16.36.48@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/6ea0b3dc-67ab-e166-f9c9-52d9ec6ff2a3.png)

さて、`FlowからSchedule Sendで送信`図の⑤のFlow、これがSchedule Sendの肝です。最初のComposerを呼び出したFlowはEmailMessageを作成してScheduled_Datetime__cを入力した状態で終了しています。

二つ目のFlowは`Record-Triggered Flow`で、EmailMessageがScheduled_Datetime__cを設定された時に起動するよう設定します。Send EmailアクションはAsyncronous Pathで、起点となるDatetime情報を`Scheduled_Datetime__c`に指定します。起動までの時間はafter 0 minとします。

![CleanShot 2024-03-16 at 17.29.25@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/886dd9b7-ffd7-49bf-0dc3-f38566c73cd2.png)


#### Send Emailアクションの呼び出し
Flow内には標準でSend Emailアクションがあります。
![CleanShot 2024-03-16 at 15.07.25@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/6cb96bac-550f-05ed-7904-6e717bd9066e.png)
`Recipient Id`や`Recipient Address Collection`など、送信先の設定でコツが入りますが、前述の通りContactId指定かアドレス指定かの違いです。`Log Email on Send`で送信後の関連ログの有無を指定します。輪っかのマークはBooleanでtrueかfalseの指定です。

また宛先指定がRecipeintIdかRecipeint Address Listなのかを指定します。直接入力かContactPickUpかを選択できるようなチェックボックスを追加してもいいかもしれません。

![CleanShot 2024-03-16 at 15.32.05@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/08b74d00-cbb6-1080-c81e-c52193a69445.png)


また、Send Emailで送信時`Log Email on Send`をtrueにして関連レコードに一貫性を持たせるとこの下書きが残ったまま新たにログは発生するので、下書きはフローの最後で削除します。

### 検証
検証する際、Sandboxの設定でDeliverabilityを調整します。System email onlyではなくAll emailにするとEmail Sendが実際に行われます。

#### Deliverabilityの設定 （検証が終わればSystem email onlyに戻す）
![CleanShot 2024-03-17 at 12.37.58@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/27974a3f-16a3-e3b9-0042-e8c1076e86d1.png)

#### Send Laterボタンの配置
作成したFlowをQuick Actionボタンとして作成した際、EmailMessageのPageLayoutで`Mobile & Lightning Actionsで配置を行うとQuick Actionからコマンド可能になります。

![CleanShot 2024-03-17 at 14.07.35@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/f4cebe2a-bdfe-6ac0-6444-37b59857afa8.png)

Activityのタイムラインにもボタンが出現します。
![CleanShot 2024-03-17 at 14.21.38@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/7ee46d3c-f555-3ad7-e76d-75e7d8b11c0c.png)


### Apexでの実装
以上で基本的機能はカバーされていますがApexの実装は、より柔軟性を持った開発が行えます。実装にあたってはこちらでMethodを作成してLWCやAuraからDatatableのRow Actionから返信したり、Modalで開いたのち送受信履歴から選択的に返信したりできます。

また添付ファイル付きの定型文など添付ファイルを扱う場合全体でも部分的でもApexが必要になるかと思います。

::: note info
[SingleEmailMessage Methods](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_classes_email_outbound_single.htm)
こちらでSingleEmailMessageの概要が説明されています。

[Email Class (Base Email Methods)](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_classes_email_outbound_base.htm#apex_classes_email_outbound_base)
こちらでsetSaveAsActivityの挙動の記載がありますが、フロー内の標準コンポーネントでそのtrue, falseの設定が可能になっています。Apexで送る場合は、こちらを制御することで自動的に関連レコードを作成するかしないかを制御できます。（データの一貫性のためあえてfalseにする必要はないかと思います）
:::

以下のサンプルでは、条件分岐などを多少省略していますが、基本はMapでmailのconfigをapexに渡してmailを条件に応じてsetしていきます。設定後送信するかFlowに載せるためだけのEmailMessage作成にするか、添付はAttachment保存なのかContentDocument保存なのかは、利用用途に応じて柔軟に変更できます。 Invocable Methodを作成することによりFlowのエレメントとして利用が可能になります。

![CleanShot 2024-03-17 at 13.54.54@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15524/afc9ea45-17e3-1fcc-fef8-dc7eccc7afd1.png)


<details><summary>サンプル (Send Email by Apex)</summary>

``` java:apex
@AuraEnabled
public static void sendEmail(Map<String, String> config) {
    Id orgWideEmailAddressId = [SELECT Id, Address, DisplayName FROM OrgWideEmailAddress WHERE Address = :config.get('orgWideEmailAddress')].Id;
    Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

    // mailの設定
    mail.setTargetObjectId(config.get('contactId')); 
    mail.setOrgWideEmailAddressId(orgWideEmailAddressId);
    mail.setSubject(config.get('subject'));
    if (config.get('ccAddress') != null) {
    List<String> ccAddresses = config.get('ccAddress').split(',');
    mail.setCcAddresses(ccAddresses);
    }
    mail.setHtmlBody(config.get('htmlBody'));
    mail.setWhatId(config.get('relatedToId')); 

    // 添付ファイルの設定
    List<String> documentIds = config.get('documentIds').split(',');
    List<Messaging.EmailFileAttachment> emailAttachments = new List<Messaging.EmailFileAttachment>();
    List<ContentVersion> contentVersions = [
    SELECT Title, FileType, VersionData 
    FROM ContentVersion 
    WHERE ContentDocumentId IN :documentIds AND IsLatest = true
    ];

    for (ContentVersion contentVersion : contentVersions) {
    Messaging.EmailFileAttachment emailAttachment = new Messaging.EmailFileAttachment();
    emailAttachment.setFileName(contentVersion.Title + '.' + contentVersion.FileType);
    emailAttachment.setBody(contentVersion.VersionData);
    emailAttachment.setContentType(mimeTypes.get(contentVersion.FileType));
    emailAttachments.add(emailAttachment);
    }
    mail.setFileAttachments(emailAttachments);// こちらでメールに添付を指定

    // mail送信
    Messaging.SendEmailResult[] results = Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
```
</details>

<details><summary>サンプル (Schedule Send by Invocable Apex)</summary>

```java:apex
@InvocableMethod
public static void sendEmailAttachment(List<inputParam> param) {
    // 略）InputParamなど emailの設定は上記サンプルを参照
    // ...
    List<Messaging.SendEmailResult> result =  Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
    EmailMessage emailMessage = [SELECT Id FROM EmailMessage WHERE ParentId = :param[0].parentId ORDER BY CreatedDate DESC LIMIT 1];
    if (!(fileIds.isEmpty())) {
        List<String> documentIds = param[0].contentDocumentIds;
        for (String documentId : documentIds) {
            ContentDocumentLink cdl = new ContentDocumentLink();
            cdl.ContentDocumentId = documentId;
            cdl.LinkedEntityId = emailMessage.Id; 
            cdl.ShareType = 'V'; 
            insert cdl;
        }
    }
}
```
</details>

JavaScript側のConfigはObjectで構成してApexに渡します。ConfigはJavaScript側で編集が柔軟になります。返信用のためSubjectに`Re:`を足したり前回の内容を`引用`で囲ったり、相手先をcontactIdにするかダイレクトのEmailAddressesにするのか、cc:は複数あるのか、添付のcontentDocumentIdsはあるか、等々かなり柔軟に拡張ができます。

また最大の利点はスピードです。Flowでの実装よりサクサク動きます。日常的使用する頻度が高ければまずFlowで実装してからApex側に同じ使い勝手の実装を行うととてもスムーズに移行できます。

## Email-to-CaseにおけるRef IdやThread Token
CaseからですとSend Email時にThread Tokenを挿入してくれますが、Apexからの送信の際にSubjectやTextBodyする必要があります。こちらに詳細の実装を記載しているの参考までに。

https://qiita.com/masahiro_sf/items/298c7803d8e62425cd61

# まとめ
汎用的なSend Email with Attachmentsがあると活躍の場所が増えるかと思います。特に、FileUploadなどで添付ファイルをつけるケースや定型分の差し込みなどがApexとLWCを駆使して行うとより滑らかな体験になります。Scheduled Datetimeの将来の時刻してのValidationや、Modal Componentで表示しているEmailMessage 履歴から返信Componentの作成、テンプレのLoad等、キーダウン操作によるメールリスト上でのQuick Look閲覧など可能です。

詳細の設定は記事内では省略しているので適宜必要な項目に値を充当していただければと思います。