WPF(.NET)アプリでレポートや帳票を 物理ドライブに保存せず、そのままメールに添付して送信したい——業務アプリではごく普通の要件です。ところが実装してみると、MemoryStream に書き込めない、送ったはずの添付が空、Attachment に渡したのに添付されない、SmtpClient が通らない……といった“あるある”にぶつかりがちです。本記事は、この問題の本質と最短で解決する実装パターン、運用でハマらないための注意点を実務目線で詳解します。
結論(TL;DR)
- Excelは ClosedXML 等のストリーム対応ライブラリで作る。
workbook.SaveAs(ms)の直後にms.Position = 0;を必ず実行。- 送信は MailKit(または Microsoft Graph)へ移行。
System.Net.Mail.SmtpClientは認証要件的に厳しい。 - 添付の MIME タイプ は
application/vnd.openxmlformats-officedocument.spreadsheetml.sheetを明示。 - ストリームや添付は Dispose管理(
using)を徹底。UIは async/await でブロックしない。
状況とよくある課題
やりたいこと
- WPFのボタン(例:「メール送信」)で、アプリ内で生成したExcelを添付して送信したい。
- セキュリティポリシー上、物理ファイルを作らずに
MemoryStream上で完結したい。
発生しがちなトラブルの実例
| 症状 | ありがちな原因 | 対処 |
|---|---|---|
workBook.SaveAs(ms) で例外 | 使っているExcelライブラリがストリーム保存をサポートしていない/ms が書き込み不可または ms を既にDispose | ClosedXMLなどストリーム対応のライブラリを使う/new MemoryStream() を適切に管理 |
| 添付が空/壊れている | ms.Position を先頭に戻していない/GetBuffer() の使い方誤り | ms.Position = 0; を必ず実行/ToArray() を使う |
Attachment(Stream) で送ったのに添付されない | 送信前に ms をDispose/MIME未設定/SmtpClient が途中で落ちる | 送信完了まで ms を生かす/MIMEを明示/MailKit等に移行 |
| SMTP認証で失敗 | SmtpClient がモダン認証(OAuth2)非対応 | MailKit + OAuth2 か Microsoft Graph を使用 |
最も確実な実装パターン:ClosedXML → MemoryStream → MailKit
以下は保存なしでExcelを作成し、そのままメールに添付して送る最短コードです。ClosedXMLはストリーム出力を正式サポートしており、MemoryStreamに対するSaveAsで問題が生じにくいのがメリットです。
using System;
using System.IO;
using System.Threading.Tasks;
using ClosedXML.Excel;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
public static class MailWithExcel
{
public static async Task SendAsync()
{
// 1) Excel 作成(ClosedXML)
using var wb = new XLWorkbook();
var ws = wb.Worksheets.Add("Sheet1");
ws.Cell("A1").Value = "Hello world!";
ws.Cell("B2").Value = DateTime.Now;
using var ms = new MemoryStream();
wb.SaveAs(ms); // ストリームに保存
ms.Position = 0; // ★必須:先頭に戻す
// 2) メール作成(MimeKit)
var message = new MimeMessage();
message.From.Add(MailboxAddress.Parse("[email protected]"));
message.To.Add(MailboxAddress.Parse("[email protected]"));
message.Subject = "レポート送付(保存なし添付)";
var builder = new BodyBuilder { TextBody = "レポートを添付します。" };
// 添付:MIMEタイプを明示
var contentType = new ContentType("application",
"vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// メモリ複製を避けたい場合は Stream 版も可だが、ToArray() の方が管理が簡単
builder.Attachments.Add("Report.xlsx", ms.ToArray(), contentType);
message.Body = builder.ToMessageBody();
// 3) 送信(MailKit)
using var client = new SmtpClient();
await client.ConnectAsync("smtp.example.com", 587, SecureSocketOptions.StartTls);
// 認証(例:ユーザ名/パスワード)※実運用はOAuth2推奨
await client.AuthenticateAsync("smtp-user", "smtp-password");
await client.SendAsync(message);
await client.DisconnectAsync(true);
}
}
ポイント
ms.Position = 0;を入れないと、添付が空になるか、壊れます。- MailKitの
BodyBuilder.Attachments.Add()にbyte[]を渡すと、送信後にMemoryStreamをDisposeしても安全です(内部で必要分を保持)。 - 添付のMIMEは必ず明示。Excel(.xlsx)は
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet。
「保存なし添付」がうまくいかない原因と対策を体系化
| 失敗ポイント | 詳細 | ベストプラクティス |
|---|---|---|
| Excel生成ライブラリの選定 | Interopや一部の古いライブラリは「ファイル保存前提」。 ストリーム保存が曖昧だと例外や壊れた添付になりやすい。 | ClosedXML / EPPlus / NPOIなどストリーム保存OKのライブラリを利用。 |
| ストリームの位置 | SaveAs(ms) 後はストリームの位置が末尾。そのまま読み出すと0バイト(空添付)。 | ms.Position = 0 を必ず行う。 |
| Disposeタイミング | Attachment に渡したストリームを先にDisposeすると送信時に読み出せず失敗。 | 送信完了後にDispose。 または ToArray()でバッファを渡す。 |
| MIMEタイプ | 未指定だと application/octet-stream になり、環境によって扱いが不安定。 | ExcelのMIMEを明示。 ファイル名拡張子も .xlsx を付ける。 |
| SMTPの認証方式 | モダン環境はOAuth2必須が主流。SmtpClient は基本認証のみで躓く。 | MailKit + OAuth2、またはMicrosoft Graphへ移行。 |
代替:System.Net.Mail(最小構成)の正しい書き方
レガシー互換や社内SMTPでどうしても System.Net.Mail を使う場合の最小コード。認証・運用上はMailKit推奨です。
using System.IO;
using System.Net;
using System.Net.Mail;
using System.Net.Mime;
using ClosedXML.Excel;
void SendWithSystemNetMail()
{
using var wb = new XLWorkbook();
var ws = wb.Worksheets.Add("Data");
ws.Cell("A1").Value = "保存なしメール添付テスト";
using var ms = new MemoryStream();
wb.SaveAs(ms);
ms.Position = 0; // ★必須
using var mail = new MailMessage("[email protected]", "[email protected]");
mail.Subject = "テスト送信";
mail.Body = "保存無しで添付しています。";
var ct = new ContentType("application",
"vnd.openxmlformats-officedocument.spreadsheetml.sheet");
var attachment = new Attachment(ms, ct);
attachment.Name = "Report.xlsx"; // ファイル名
attachment.ContentDisposition.FileName = "Report.xlsx";
mail.Attachments.Add(attachment);
using var smtp = new SmtpClient("smtp.example.com", 587)
{
EnableSsl = true,
Credentials = new NetworkCredential("user", "password")
};
smtp.Send(mail); // ここでmsはまだ生きている必要がある
}
注意:using(var ms = ...) のスコープを smtp.Send() より外にしてしまうと、送信時にストリームが既に閉じられてエラーになります。必ず送信完了後にDisposeされる構造にしてください。
Microsoft 365に強い:Microsoft Graphで送信(OAuth2)
Microsoft 365(Exchange Online)の場合、SMTPよりもGraph API(/sendMail)が確実でモダンです。WPFなら Azure.Identity の InteractiveBrowserCredential などを使ってトークンを取得し、そのまま添付送信できます。
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using ClosedXML.Excel;
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Graph.Models;
public static class GraphMailer
{
public static async Task SendViaGraphAsync()
{
// 1) Excel生成 → メモリ
using var wb = new XLWorkbook();
var ws = wb.Worksheets.Add("Report");
ws.Cell("A1").Value = "Graph送信";
ws.Cell("A2").Value = DateTime.Now;
using var ms = new MemoryStream();
wb.SaveAs(ms);
var bytes = ms.ToArray(); // Graphはbyte[]でOK
// 2) Graphクライアント(ユーザー委任 /Mail.Send)
var scopes = new[] { "Mail.Send" };
var cred = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions
{
TenantId = "common", // またはテナントID
ClientId = "YOUR-APP-CLIENT-ID",
RedirectUri = new Uri("http://localhost")
});
var graph = new GraphServiceClient(cred, scopes);
// 3) メッセージ作成(添付:FileAttachment)
var msg = new Message
{
Subject = "Graphで保存なし添付",
Body = new ItemBody { ContentType = BodyType.Text, Content = "添付をご確認ください。" },
ToRecipients = new List<Recipient>
{
new Recipient { EmailAddress = new EmailAddress { Address = "[email protected]" } }
},
Attachments = new List<Attachment>
{
new FileAttachment
{
OdataType = "#microsoft.graph.fileAttachment",
Name = "Report.xlsx",
ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
ContentBytes = bytes
}
}
};
await graph.Me.SendMail.PostAsync(new Microsoft.Graph.Users.Item.SendMail.SendMailPostRequestBody
{
Message = msg,
SaveToSentItems = true
});
}
}
ポイント
- Azure AD(Entra ID)アプリ登録で
Mail.Send権限を付与。ユーザー委任なら同意が必要。 - SMTPの制約やTLS設定に悩まされず、モダン認証一本化ができるため運用が楽です。
使うライブラリ別:ストリーム保存の可否と実装メモ
| ライブラリ | ストリーム保存 | サンプル | 備考 |
|---|---|---|---|
| ClosedXML | ◎ 対応 | wb.SaveAs(ms); ms.Position = 0; | 操作が直感的。レイアウト・書式も簡単。 |
| EPPlus | ◎ 対応 | package.SaveAs(ms); ms.Position = 0; | 商用利用時はライセンスに注意。LicenseContext の設定が必要。 |
| NPOI | ◎ 対応 | wb.Write(ms); ms.Position = 0; | 大規模データで堅実。軽量志向。 |
| Office Interop | △ 基本はファイル保存 | – | サーバーサイド非推奨。UIプロセス依存で不安定。 |
WPF(MVVM)での実装ガイド:UIフリーズを回避する
Excel生成や送信は重い処理です。UIスレッドで同期実行するとフリーズします。非同期(async/await) を徹底し、キャンセル・進捗も考慮しましょう。
// ICommand実装は簡略化
public AsyncRelayCommand SendReportCommand { get; }
public ViewModel()
{
SendReportCommand = new AsyncRelayCommand(SendReportAsync, CanSend);
}
private bool CanSend() => !IsBusy;
private async Task SendReportAsync()
{
try
{
IsBusy = true;
Status = "レポート作成中...";
// Excel生成→メール送信(上記メソッドを呼ぶだけ)
await MailWithExcel.SendAsync();
Status = "送信完了";
}
catch (Exception ex)
{
Status = "失敗: " + ex.Message;
// ログ出力
}
finally
{
IsBusy = false;
}
}
Tips
- ユーザーの操作性:プログレス表示、キャンセルボタン、エラーメッセージの整備。
- 例外の握り潰し禁止:ログ(イベントログやファイル)へ確実に出す。
MailKitを使うなら:プロトコルログで“原因究明が一瞬”
通信失敗時はプロトコルログが有効です。MailKitは簡単に記録できます。
using var logStream = new MemoryStream();
using var logger = new ProtocolLogger(logStream);
using var client = new MailKit.Net.Smtp.SmtpClient(logger);
await client.ConnectAsync("smtp.example.com", 587, SecureSocketOptions.StartTls);
// ...
// 送信処理
// ...
// 失敗時:ログ確認
logStream.Position = 0;
using var reader = new StreamReader(logStream);
var protocolText = reader.ReadToEnd();
// ログをファイル出力やアプリ内画面に表示する
大容量・高頻度送信の設計ポイント
- サイズ上限:SMTPや受信側の上限に注意(本文+Base64エンコードで実体サイズの約1.37倍)。
- 運用代替:サイズが大きい/頻度が高い場合、OneDrive/SharePointへアップロードしてリンク送信が現実的。
- メモリ効率:巨大ワークブックは
ToArray()での複写コストが無視できない。
MailKitのMimePart+MimeContent(Stream)でストリームを直接渡す手もある(Dispose順序に注意)。
日本語ファイル名・文字化け対策
- ファイル名は
attachment.NameとContentDisposition.FileNameの両方に設定(System.Net.Mail)。 - MailKitはRFC2231拡張でエンコードされ、主要メーラーで安定して表示されます。
セキュリティと資格情報の扱い
- メール資格情報やクライアントシークレットはソースに直書きしない。Windows資格情報マネージャー、ユーザーシークレット、Azure Key Vault等で管理。
- OAuth2トークンのキャッシュは暗号化して保存。リフレッシュ・失効の扱いを設計。
- 監査証跡(送信記録)をアプリ側でも保持すると、問い合わせ対応が楽になります。
よくある落とし穴チェックリスト
ms.Position = 0を忘れていないか。- 送信が終わる前に
MemoryStreamをDisposeしていないか。 - Excelの MIMEタイプ を指定しているか。
- SMTPがモダン認証必須の環境で
SmtpClientを使い続けていないか。 GetBuffer()をそのまま使って余分なバイトを送っていないか(ToArray()を推奨)。- UIスレッドで同期実行してフリーズしていないか(
async化)。
一時ファイル経由という逃げ道(どうしてもストリーム非対応なら)
特殊要件や使用ライブラリの制約でストリーム保存ができないケースでは、一時ファイルを安全に使う方法もあります。ただし、I/Oコストと情報漏えいリスクが上がるため、可能なら避けましょう。
var tmp = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".xlsx");
try
{
// ライブラリの都合で一度ファイルに保存
wb.SaveAs(tmp);
// 読み込んでMemoryStream化
byte[] bytes = File.ReadAllBytes(tmp);
var builder = new BodyBuilder { TextBody = "添付します。" };
builder.Attachments.Add("Report.xlsx", bytes,
new ContentType("application", "vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
// 送信...
}
finally
{
if (File.Exists(tmp))
{
// 確実に削除
File.Delete(tmp);
}
}
デバッグとテストのコツ
- 最初は自分宛に送信して、受信側(Gmail/Outlookなど)で「開けるか/壊れていないか」を確認。
- 実運用前にサイズ上限・回数制限・送信ドメイン認証(SPF/DKIM/DMARC) の影響を運用部門と事前合意。
- ログは成功時も残す(後日“届いていない”の問い合わせ時に有効)。
サンプル:ClosedXMLでのExcel整形の基本
using var wb = new XLWorkbook();
var ws = wb.Worksheets.Add("売上");
ws.Cell("A1").Value = "日付";
ws.Cell("B1").Value = "商品";
ws.Cell("C1").Value = "金額";
ws.Range("A1:C1").Style.Font.Bold = true;
// サンプルデータ
for (int i = 0; i < 10; i++)
{
ws.Cell(i + 2, 1).Value = DateTime.Today.AddDays(-i);
ws.Cell(i + 2, 2).Value = "アイテム" + (i + 1);
ws.Cell(i + 2, 3).Value = 1200 + i * 100;
}
ws.Column(1).Style.DateFormat.Format = "yyyy-mm-dd";
ws.Column(3).Style.NumberFormat.Format = "#,##0";
ws.Columns().AdjustToContents();
// 以降は SaveAs(ms) → メール添付へ
運用で差が出る小ワザ集
- 件名・本文に識別子(対象日付、レポート種別、部署名など)を入れて検索性を高める。
- 送信履歴テーブル(宛先・件名・時刻・結果・メッセージID)をDBに残す。
- リトライ戦略:一時的な接続エラーは指数バックオフで再送。二重送信防止のキー設計。
- 例外のユーザー向けメッセージは平易な日本語で(技術詳細はログへ)。
まとめ
WPFアプリから「ファイルを保存せずに」Excelをメール添付するには、ストリーム対応のExcelライブラリ(ClosedXML等)と、モダンな送信手段(MailKitまたはMicrosoft Graph)を組み合わせるのが最短・最安定です。特に、ms.Position = 0 の徹底、MIMEタイプの明示、Disposeの順序管理は“壊れた添付”や“添付なし”を未然に防ぐ必須要件です。認証面ではモダン化が進んでいるため、実運用ではOAuth2への移行を前提に設計しておくと将来の保守コストが下がります。
付録:トラブルシューティング早見表
| 現象 | 即確認するポイント | 対応 |
|---|---|---|
| 添付が0バイト | ms.Position が0か? | ms.Position = 0; を実行。 |
| 送信時に例外 | ストリームを先にDisposeしていないか? | 送信完了まで ms を生かす/ToArray() を使う。 |
| 受信側で開けない | MIMEタイプ/拡張子が正しいか? | .xlsx とExcelのMIMEを明示。 |
| SMTP認証が通らない | OAuth2が必須か? 基本認証が拒否されていないか? | MailKit + OAuth2 または Graphへ移行。 |
完全版サンプル(ClosedXML + MailKit、WPFから呼び出しやすい形)
using System;
using System.IO;
using System.Threading.Tasks;
using ClosedXML.Excel;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
public sealed class ExcelMailService
{
private readonly string _smtpHost;
private readonly int _smtpPort;
private readonly string _smtpUser;
private readonly string _smtpPassword;
```
public ExcelMailService(string host, int port, string user, string password)
{
_smtpHost = host;
_smtpPort = port;
_smtpUser = user;
_smtpPassword = password;
}
public async Task SendReportAsync(string to, string subject)
{
// Excel生成
using var wb = new XLWorkbook();
var ws = wb.Worksheets.Add("Sheet1");
ws.Cell("A1").Value = "保存無しでExcel添付";
ws.Cell("A2").Value = DateTime.Now;
ws.Columns().AdjustToContents();
using var ms = new MemoryStream();
wb.SaveAs(ms);
ms.Position = 0;
// メール作成
var message = new MimeMessage();
message.From.Add(MailboxAddress.Parse("[email protected]"));
message.To.Add(MailboxAddress.Parse(to));
message.Subject = subject;
var builder = new BodyBuilder { TextBody = "添付をご確認ください。" };
var ct = new ContentType("application",
"vnd.openxmlformats-officedocument.spreadsheetml.sheet");
builder.Attachments.Add("Report.xlsx", ms.ToArray(), ct);
message.Body = builder.ToMessageBody();
// 送信
using var client = new SmtpClient();
await client.ConnectAsync(_smtpHost, _smtpPort, SecureSocketOptions.StartTls);
await client.AuthenticateAsync(_smtpUser, _smtpPassword);
await client.SendAsync(message);
await client.DisconnectAsync(true);
}
```
}
上記クラスをDIやサービスとしてViewModelから呼び出すだけで、WPFアプリに綺麗に組み込めます。
最後に:この手順で“もう迷わない”
- ExcelはClosedXML、保存はMemoryStream、位置リセットが必須。
- 送信はMailKit(またはGraph)、MIME・Dispose・非同期で安定運用。
- サイズ・認証・ログ・UI応答性を最初から設計に入れる。
参考の実装差分(メール送信手段の比較)
| 手段 | 強み | 弱み | 向いているケース |
|---|---|---|---|
| MailKit(SMTP) | 実装が簡単、ログ豊富、OAuth2対応可 | SMTP上限・運用ポリシーに左右される | 既存SMTPがある、移行コストを抑えたい |
| Microsoft Graph | Microsoft 365と親和性高、モダン認証、監査性 | アプリ登録・権限同意など初期設定が必要 | Exchange Online主体、将来の運用を見据える |
| System.Net.Mail(レガシー) | レガシー互換 | モダン認証非対応、将来性に乏しい | 閉域・オンプレ専用や一時的な互換維持 |
FAQ(現場でよく聞かれる質問)
Q. SaveAs(ms) で「書き込みできません」の例外が出ます。
A. ms が読み取り専用でないか、または使用ライブラリがストリーム保存を未サポートの可能性。ClosedXML/EPPlus/NPOI等に切り替えてください。ms を new MemoryStream() で生成し直して、他のコードで閉じていないかも確認。
Q. 添付ファイルが空になります。
A. ほぼ確実に ms.Position = 0 忘れ。
または送信前に ms をDisposeしているか、GetBuffer() を使って余計な領域を送っている可能性。
Q. Gmail/Microsoft 365 で認証に失敗します。
A. 環境側で基本認証が封じられているケースが多いです。MailKit + OAuth2、またはMicrosoft Graphへの移行を検討してください。
Q. 日本語ファイル名が文字化けします。
A. System.Net.Mailの場合は Name と ContentDisposition.FileName を両方設定。MailKitは自動でRFCに沿ったエンコードを行うため安定します。
チェックサンプル:NPOI/EPPlusでの保存なし添付(抜粋)
NPOI
using NPOI.XSSF.UserModel;
var wb = new XSSFWorkbook();
var sheet = wb.CreateSheet("Sheet1");
var row = sheet.CreateRow(0);
row.CreateCell(0).SetCellValue("NPOI");
// 保存なし
using var ms = new MemoryStream();
wb.Write(ms, true);
ms.Position = 0;
// 以降はMailKitで送信(同様)
EPPlus
using OfficeOpenXml;
// EPPlus では事前にライセンスコンテキスト設定が必要な場合あり
// ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
using var package = new ExcelPackage();
var ws = package.Workbook.Worksheets.Add("Sheet1");
ws.Cells["A1"].Value = "EPPlus";
using var ms = new MemoryStream();
package.SaveAs(ms);
ms.Position = 0;
// 以降はMailKitで送信(同様)
実装テンプレ・貼り付け用(ClosedXML + MailKit)
最初の実装を素早く形にしたい場合は、以下をそのままプロジェクトに追加して呼び出せば動きます。
public class ReportMailer
{
private readonly string _host; private readonly int _port;
private readonly string _user; private readonly string _pass;
public ReportMailer(string host, int port, string user, string pass)
{ _host = host; _port = port; _user = user; _pass = pass; }
public async Task SendAsync(string to)
{
using var wb = new ClosedXML.Excel.XLWorkbook();
var ws = wb.Worksheets.Add("Report");
ws.Cell("A1").Value = "保存なしメール添付";
ws.Cell("A2").Value = DateTime.Now;
ws.Columns().AdjustToContents();
using var ms = new MemoryStream();
wb.SaveAs(ms); ms.Position = 0;
var message = new MimeMessage();
message.From.Add(MailboxAddress.Parse("[email protected]"));
message.To.Add(MailboxAddress.Parse(to));
message.Subject = "保存なし添付のサンプル";
var builder = new BodyBuilder { TextBody = "添付をご確認ください。" };
var ct = new ContentType("application","vnd.openxmlformats-officedocument.spreadsheetml.sheet");
builder.Attachments.Add("Report.xlsx", ms.ToArray(), ct);
message.Body = builder.ToMessageBody();
using var client = new MailKit.Net.Smtp.SmtpClient();
await client.ConnectAsync(_host, _port, SecureSocketOptions.StartTls);
await client.AuthenticateAsync(_user, _pass);
await client.SendAsync(message);
await client.DisconnectAsync(true);
}
}
WPFのボタンハンドラやコマンドから await new ReportMailer(...).SendAsync("[email protected]"); と呼ぶだけでOKです。
これで“ファイルを保存せずに”送る要件はクリアできる
本記事のとおりに進めれば、物理ファイルを一切残さず、添付が壊れない、モダン認証に対応した堅牢な実装を短時間で組み込めます。あとは、業務要件に合わせてExcelのレイアウトや本文、エラーハンドリングを肉付けすれば完成です。
付記:最低限のチェックポイント(再掲)
- ClosedXMLなどストリーム対応ライブラリで生成する。
ms.Position = 0を忘れない。- MIMEタイプはExcelのものを指定。
- 送信はMailKit/Graphに寄せ、Dispose順序と非同期を守る。
以上、WPFアプリで「保存なし」でExcelをメール添付できない問題の原因の見極め→正しいパターン→運用の勘所までを一気通貫で整理しました。現場の実装にそのまま活かしてください。

コメント