Apex

アップロード済みファイルを添付してメールを送る実装

取引先にアップロードしたファイルを、メール送信時に取り直してファイル名を変え、SingleEmailMessageに添付して送ります。添付サイズと送信件数の上限を説明します。

2024.02.01

シナリオ

  1. 取引先にファイルをアップロードする

    あらかじめ取引先の「ファイル」関連リストへ、メールに添付したいファイルをアップロードしておきます。

  2. メール送信時にファイルを取得する

    アップロード済みのContentVersionを取得し、ファイル名を変更したうえでメールに添付する準備をします。

  3. メールを送信する

    取引先のリストビューから対象レコードを選び、ボタンからメールを送信します。

プログラム

SendEmailAccountFile.page

<apex:page standardController="Account" recordSetVar="records" lightningStyleSheets="true" extensions="SendEmailAccountFile">
    <apex:pageMessages id="pageMessages" />
    <apex:form >
        <apex:pageBlock title="send test email" rendered="{!isNotEmpty}">
            <apex:pageBlockButtons location="top" rendered="{!isNotEmpty}">
                <apex:commandButton value="send" action="{!sendEmail}" reRender="pageMessages" disabled="{!NOT(isSendable)}"
                />
                <apex:commandButton value="cancel" action="{!cancel}"/>
            </apex:pageBlockButtons>
            <apex:pageBlockTable value="{!accounts}" var="account" rendered="{!isNotEmpty}">
                <apex:column headerValue="Name" value="{!account.Name}" />
            </apex:pageBlockTable>
        </apex:pageBlock>
    </apex:form>
</apex:page>

取引先のリストビューから複数選択して呼び出すVisualforceページです。エラーはpageMessagesにまとめて表示します。

SendEmailAccountFile.cls

public with sharing class SendEmailAccountFile {
    private final Pagereference returnRef;
    private final List<Account> accounts;
    public SendEmailAccountFile(ApexPages.StandardSetController controller) {
        Set<Id> accountIds = (new Map<Id, sObject>(controller.getSelected())).keySet();
        if (accountIds.size() >= 1 && accountIds.size() <= Limits.getLimitEmailInvocations()) {
            accounts = [SELECT Name, Email__c FROM Account WHERE Id IN :accountIds AND Email__c != null];
        }
        if (accounts?.size() < 1) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Email destination does not exist'));
        }
        returnRef = null;
    }
    public List<Account> getaccounts() { return accounts; }
    public Boolean getIsNotEmpty() { return accounts?.size() > 0; }
    public Boolean getIsSendable() { return !ApexPages.hasMessages(); }
    public class Email {
        public final String fromAddress;
        public final String toAddress;
        public final String subject;
        public final String bodyHtml;
        public final String bodyText;
        public final List<Id> contentVersionIds;
        public final ContentVersion cv;
        public Email(Id accountId) {
            Account account = [SELECT Name, Email__c FROM Account  WHERE Id = :accountId  AND Email__c != null LIMIT 1 ];
            User user = [SELECT Name FROM User WHERE Id = :UserInfo.getUserId() ];
            this.fromAddress = UserInfo.getUserEmail();
            this.toAddress = account.Email__c;
            this.subject = 'test';
            String[] bodyParts = new List<String> {
                '',
                'test',
                ''
            };
            String body = String.join(bodyParts, '\n');
            this.bodyHtml = body.replace('\n', '<br/>');
            this.bodyText = body.stripHtmlTags();
            Set<Id> linkedEntityIds = new Set<Id>{ account.Id };
            List<ContentDocumentLink> links = [SELECT Id, LinkedEntityId, ContentDocumentId, ContentDocument.LatestPublishedVersionId, ContentDocument.title FROM ContentDocumentLink WHERE LinkedEntityId IN :LinkedEntityIds ];
            List<Id> documentIds = new List<Id>();
            for (ContentDocumentLink link : links) {documentIds.add(link.ContentDocumentId);}
            List<ContentVersion> targetCVs = new List<ContentVersion>();
            String fileName = 'test1';
            String fileExtension = 'pdf';
            targetCVs = [SELECT Id, Title, FileType, FileExtension, VersionNumber, VersionData, ContentDocumentId FROM ContentVersion WHERE IsLatest = true AND ContentDocumentId IN :documentIds AND Title = :fileName AND FileExtension = :fileExtension ORDER BY CreatedDate Desc ];
            this.contentVersionIds = new List<Id>();
            if(targetCVs.size() > 0){
                this.cv = new ContentVersion(Title = 'test', PathOnClient = 'test.pdf', VersionData = targetCVs.get(0).VersionData, OwnerId = UserInfo.getUserId());
            }
        }
    }
    public Pagereference SendEmail() {
        try {
            for (Account account : accounts) {
                Email email = new Email(account.Id);
                if(email.cv != null){
                    insert email.cv;
                    email.contentVersionIds.add(email.cv.Id);
                }
                Messaging.SingleEmailMessage singleEmail = new Messaging.SingleEmailMessage();
                singleEmail.setToAddresses(new List<String>{ email.toAddress });
                if(email.contentVersionIds.size() > 0){
                    singleEmail.setSubject(email.subject);
                    singleEmail.setHtmlBody(email.bodyHtml);
                    singleEmail.setPlainTextBody(email.bodyHtml.stripHtmlTags());
                    singleEmail.setEntityAttachments(email.contentVersionIds);
                    singleEmail.setBccSender(true);
                    Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ singleEmail });
                }
            }
        } catch(Exception e) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL, e.getStackTraceString()));
            return null;
        }
        return null;
    }
}

選択した取引先ごとに、アップロード済みのファイルを取得してメールへ添付し送信します。添付が見つからない取引先へはメールを送りません。

内部のEmailクラスが、取引先に紐づくContentDocumentLinkからContentVersionを検索し、TitleとFileExtensionが一致する最新版を取得しています。そのうえで、VersionData(ファイルの中身)だけを取り出し、TitleとPathOnClientをtest.pdfに変えた新しいContentVersionを作っています。setEntityAttachmentsでファイル名を変えて送るには、こうして中身だけを引き継いだコピーを作ります。作ったコピーは、組織にファイルとして残ります。レコードを増やさずに名前だけ変えたいときは、Messaging.EmailFileAttachmentのsetFileNameで添付ファイル名を指定する方法もあります。

Messaging.SingleEmailMessageへの添付はsetEntityAttachmentsを使っています。Document・ContentVersion・AttachmentのIdをまとめて渡せるメソッドで、Messaging.EmailFileAttachmentのようにファイルの中身を改めてセットし直す必要がありません。

setEntityAttachmentsとsetFileAttachmentsは、どちらも渡した添付ファイルの合計サイズが10MBを超えないようにする必要があります。1件あたりの上限ではなく、1通のメールに含める添付ファイルすべての合計です。

添付サイズとメール送信数の上限

添付ファイルの合計サイズの上限に加えて、送信数そのものにも上限があります。ApexトリガーやApexクラス、APIからSingleEmailMessageで送るメールは、1日あたり5,000件の受信者数までという上限が組織単位でかかります。To・Cc・Bccに入れたアドレスはすべて合算されます。Trial組織はこれより少なく、既定は15件です。

このコードは、選択した取引先の数だけメールを1通ずつ送ります。リストビューで一度に数百件を選んで実行すると、その件数がそのまま受信者数としてカウントに積み上がります。他の自動化(ワークフローのメールアラートやFlowの送信メール)と合わせて、その日の上限に近づいていないかを意識してください。

ボタン作成

  1. 取引先のボタン設定を開く

    オブジェクトマネージャから取引先を選び、「ボタン、リンク、およびアクション」を開きます。

  2. 新規ボタンを作成する

    「新規ボタンまたはリンク」を押し、ラベルに「取引先添付ファイルメール送信」のような名前を入力します。表示の種類は「リストボタン」、コンテンツソースは「Visualforceページ」を選びます。

  3. 対象のページを選んで保存する

    Visualforceページの一覧からSendEmailAccountFileを選び、保存します。

カスタムボタン「取引先添付ファイルメール送信」の詳細画面。表示の種類はリストボタン
作成したボタンの定義です。表示の種類がリストボタンで、複数レコード選択用のチェックボックスが有効になっている点を見てください。

リストビュー

作成したボタンは、そのままでは画面に表示されません。検索レイアウトへ追加します。

  1. 検索レイアウトを開く

    オブジェクトマネージャの取引先から「検索レイアウト」を開き、「取引先リストビュー」の編集を選びます。

  2. 作成したボタンを追加する

    「使用可能なボタン」の一覧から作成したボタンを選び、「選択済みのボタン」へ移動します。

  3. 保存する

    保存すると、取引先のリストビュー上部にボタンが表示されるようになります。

検索レイアウトの編集画面。選択したボタンの欄に作成したボタンが入っている
検索レイアウトの編集画面です。作成したボタンが「選択したボタン」側へ移っていないと、リストビューには出てきません。

動作確認

  1. ファイルをアップロードする

    取引先タブを開き、メール送信対象のレコードで「test1.pdf」というファイルをアップロードします。

  2. 対象レコードを選ぶ

    取引先のリストビューから、ファイルをアップロードした取引先にチェックを入れます。

  3. ボタンを押す

    作成した「取引先添付ファイルメール送信」ボタンを押します。

  4. 送信する

    対象の取引先名が一覧に表示されるので、内容を確認して「send」ボタンを押します。

  5. 受信を確認する

    取引先のEmail__c項目に設定したメールアドレスで受信を確認します。添付ファイル名が「test.pdf」に変わっていることを確認します。

取引先のリストビュー。右上に追加したボタンが並び、1件にチェックが入っている
リストビューの画面です。右上に増えたボタンと、送信対象の取引先にチェックを入れた状態を見てください。
Visualforceページの画面。送信対象の取引先名が一覧に表示されている
ボタンから開いたページです。選んだ取引先が一覧に出ていることを確かめてから send を押します。
受信したメール。1個の添付ファイルとしてtest.pdfが付いている
受信したメールです。添付ファイル名がアップロード時の名前ではなく test.pdf に変わっていることを確認してください。

ここで間違えやすい

間違い何が起きるか
添付ファイルの合計サイズが10MBを超えるsetEntityAttachmentsを含めたメール送信が失敗します
対象の取引先にファイルが1件もないcontentVersionIdsが空のままなので、メール自体が送信されません(エラーにもなりません)
リストビューで大量の取引先を選ぶ選択件数がそのままその日の送信数にカウントされ、5,000件の上限に近づきます
Email__cが空の取引先を選ぶコンストラクタの絞り込みで対象から外れ、メールは送られません
ファイル名や拡張子の条件を変えずに流用するTitleとFileExtensionの一致でファイルを探しているため、条件に合うファイルがないと添付されません

確認した環境

  • 2026年9月 / Salesforce Summer '26(APIバージョン67.0)時点の公式ドキュメントで、ContentVersion・ContentDocumentLinkの仕様、setEntityAttachmentsの添付サイズ上限、SingleEmailMessageの1日あたりの送信数上限を確認しています

まとめ

  • 添付ファイルの保存と取得にはContentVersionとContentDocumentLinkを使います。Attachmentは使いません
  • setEntityAttachmentsでファイル名を変えて送る場合は、元のVersionDataだけを引き継いだ新しいContentVersionを作ります。EmailFileAttachmentのsetFileNameでも名前を変えられます
  • setEntityAttachmentsはContentVersionなどのIdを直接渡せます。添付ファイルの合計サイズは10MBまでです
  • SingleEmailMessageによるメール送信は、1日あたり5,000件の受信者数までです。To・Cc・Bccの合計でカウントされます
  • 添付ファイルが見つからないとメールは送信されません。エラーにはならないため気づきにくい挙動です

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

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