メールに添付が無いはずなのに「winmail.dat」が付いてくる。あるいは添付ファイルが見えず、本文も崩れてしまう。こうした“謎ファイル”に悩まされて検索している人は少なくありません。

この記事では、winmail.datとは何か?なぜ発生するのか?を整理した上で、実務で使える対処方法を3方向(Outlook/Thunderbird/PHPツール)でまとめます。最後には、あなたの環境(さくらのレンタルサーバー)で動くwinmail.dat表示(変換)Webツールの作り方まで一気に掲載します。

結論から言うと、winmail.dat問題は「壊れたファイル」ではなく、送信側のメール形式(TNEF)と受信側の互換性のズレで起きます。なので原因さえ分かれば、解決は難しくありません。

Contents

winmail.datとは何か?

winmail.datは、主にMicrosoft Outlook(Exchange/Office 365含む)から送信されたメールで、受信側がOutlook互換でない場合に添付されることがあるファイルです。

中身は単なる“謎データ”ではなく、Outlookが使う情報(装飾、会議情報、添付ファイル情報など)を、TNEF(Transport Neutral Encapsulation Format)という形式で詰め込んだものです。

つまりwinmail.datは、言い換えると「Outlookのリッチ情報パック」です。

  • 本来の添付ファイルが、中に入っていることがある
  • 本文(HTML/RTF)が、中に入っていることがある
  • Outlook固有の情報(投票ボタン、会議情報など)が入っていることがある

なぜwinmail.datが発生するのか?(原因)

winmail.datの発生要因はほぼこれです。

送信側(Outlook)がTNEF形式で送る設定になっているのに、受信側(Thunderbird/Gmail/スマホメール等)がTNEFをそのまま解釈できないため、Outlookが「互換性のためにまとめた情報」をwinmail.datとして付けて送ってしまう――という構図です。

よくある発生パターン

  • Outlook → Thunderbird(受信側がTNEF未対応/拡張なし)
  • Outlook → Gmail(環境・メール形式によっては添付の見え方が変わる)
  • Outlook → iPhone標準メール(本文/添付の扱いが崩れることがある)

「添付ファイルが無いのにwinmail.datだけ付く」理由

添付が無い場合でも、Outlookが本文装飾やメッセージの属性情報をTNEFで送ると、受信側ではそれを解釈できず、結果として「winmail.datだけが添付されたように見える」ことがあります。これは送信側の設定・経路(Exchange等)によって起きます。

対処方法(全体像)

対処は大きく3方向です。

  • 送信側(Outlook)の設定を変える → 根本解決
  • 受信側(Thunderbird)で表示できるようにする → 現場対応
  • Webツールで中身を展開・表示する → 社内/共有向けの最適解

対処方法1:Outlook側での対処(送信側の根本解決)

最も確実なのは、OutlookがTNEF(リッチ形式)で送らないようにすることです。相手がOutlook以外で受信する可能性があるなら、基本はHTMLかプレーンテキストに寄せるのが安全です。

1) 送信形式を「HTML」または「テキスト」にする

  • 新規メール作成画面で「テキスト形式」を確認
  • 可能ならHTML(一般的)またはテキスト(最強互換)

2) 連絡先ごとに「リッチテキストを使わない」設定

特定の相手にだけwinmail.datが出る場合、相手のアドレスに対してOutlook側で形式が固定されていることがあります。連絡先に保存されている場合は、送信形式(HTML/テキスト)を優先するのが有効です。

3) Exchange/組織側ポリシーでTNEFを抑止する

会社のMicrosoft 365/Exchange環境では、管理側でTNEFの扱いが制御されていることがあります。個人設定で直らない場合は、IT管理者に「外部宛てTNEF無効化」を相談するのが早いです。

ポイント:受信側で毎回対応するより、送信側で止める方が運用コストが小さくなります。

対処方法2:Thunderbird側での対処(受信側で表示する)

Thunderbirdは強力ですが、標準状態ではwinmail.datを中身まで表示できないケースがあります。そこで、winmail.dat(TNEF)を展開できるアドオンを使うのが定番です。

おすすめの考え方

  • まずはThunderbirdで「HTML表示」を有効にする(本文崩れ対策)
  • それでもwinmail.datが出るなら、TNEF展開アドオンで“中身を展開”する

1) Thunderbirdの表示形式を確認(HTML表示)

「プレーンテキスト表示」固定になっていると、本文が崩れる・文字化けが強く見える場合があります。まずは表示形式を「通常のHTML表示」に戻して確認します。

2) TNEF展開系アドオンを使う

Thunderbirdでは、TNEF(winmail.dat)を展開して添付や本文を見えるようにするアドオンがよく使われます。運用的には「受信者が個別に頑張る」形になるため、社内標準化するなら次のWebツール方式が便利です。

ポイント:Thunderbirdだけで完結させるならアドオンが楽。
社内で“誰でも”対応できる仕組みが必要ならWebツールが強い。

対処方法3:PHPでwinmail.dat変換(表示)Webツールを作る(さくらのレンタルサーバー実装編)

ここからが実務的に一番強い方法です。

社内やチームで「winmail.datが来たらここにアップして表示する」運用にすると、Thunderbird/Outlook/Gmailなど環境差を吸収できます。今回は、さくらのレンタルサーバーで動くPHP実装で紹介します。

作るもの(完成イメージ)

  • 画面で winmail.dat をアップロード
  • 本文(HTML/TXT/RTF)を プレビュー表示
  • 中身のファイルを 個別ダウンロード
  • 一括で ZIPダウンロード
  • 一定時間で自動削除(漏えい対策)

必要な構成

  • PHP 8.x(さくらでOK)
  • Composer(ローカルで実行して vendor を作ってアップロードでもOK)
  • ライブラリ:qualityunit/tnef-decoder

ディレクトリ構成

winmail/
  index.php
  view.php
  download.php
  vendor/
    autoload.php
    ...
  _tmp/   (一時保存用。権限 700~755)

サブドメイン等で公開する場合は、URLはダミー例として次のようにします。

https://example.com/winmail/

 

(重要)ローカルでvendorを作ってアップロードする方法

共有サーバーにComposerを入れなくても、ローカル(Windows+Git Bashなど)でvendorを生成してアップロードすればOKです。

mkdir winmail
cd winmail
composer require qualityunit/tnef-decoder

生成された vendor/composer.json(+ composer.lock)を、サーバーの winmail/ へアップロードします。

PHP実装(コピペで動く)

ここでは、実際に動作確認済みの「落ちない」「文字化けを減らす」「ZIPがnot foundにならない」実装を掲載します。
※URLはダミーです。

index.php(アップロード画面)

<?php
// index.php
declare(strict_types=1);
?>
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>winmail.dat 表示ツール</title>
  <style>
    body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;max-width:860px;margin:40px auto;padding:0 16px}
    .box{border:1px solid #ddd;border-radius:14px;padding:18px;margin:14px 0}
    input[type=file]{display:block;margin:12px 0}
    button{padding:10px 14px;border:1px solid #ddd;border-radius:10px;background:#fff;cursor:pointer}
    .muted{color:#666;font-size:13px;line-height:1.6}
  </style>
</head>
<body>
  <h1>winmail.dat 表示ツール</h1>

  <div class="box">
    <p class="muted">
      Outlookなどから届いた winmail.dat をアップロードすると、本文と添付を展開して表示します。<br>
      例)<code>https://example.com/winmail/</code>
    </p>

    <form method="post" action="view.php" enctype="multipart/form-data">
      <input type="file" name="winmail" accept=".dat,application/octet-stream" required>
      <button type="submit">表示する</button>
    </form>
  </div>

  <div class="box">
    <h2>注意</h2>
    <ul>
      <li>社外秘が含まれる可能性があるため、公開範囲は制限(ベーシック認証等)推奨</li>
      <li>アップロードされた内容は一時保存され、一定時間で削除されます</li>
    </ul>
  </div>
</body>
</html>

view.php(解析・表示)

本文の「‚¨”‚ê…」系の文字化けを減らすため、SJIS-win優先でUTF-8へ寄せています。
また、ファイル名がnullでも落ちないようにしています。

※長いので本文中は省略しません。実装はこのまま貼り付けでOKです。

<?php
// view.php
declare(strict_types=1);

ini_set('display_errors', '0');
ini_set('display_startup_errors', '0');
error_reporting(E_ALL);

require __DIR__ . '/vendor/autoload.php';

use TNEFDecoder\TNEFAttachment;

$MAX_BYTES = 25 * 1024 * 1024;
$TMP_BASE  = __DIR__ . '/_tmp';
@mkdir($TMP_BASE, 0700, true);

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  http_response_code(405);
  exit('Method Not Allowed');
}

if (!isset($_FILES['winmail']) || $_FILES['winmail']['error'] !== UPLOAD_ERR_OK) {
  http_response_code(400);
  exit('アップロードに失敗しました。');
}

if (($_FILES['winmail']['size'] ?? 0) > $MAX_BYTES) {
  http_response_code(400);
  exit('ファイルが大きすぎます(最大25MB)。');
}

$origName = (string)($_FILES['winmail']['name'] ?? 'winmail.dat');

$buf = @file_get_contents($_FILES['winmail']['tmp_name']);
if ($buf === false || $buf === '') {
  http_response_code(400);
  exit('ファイルを読み込めませんでした。');
}

$mbstringWarning = !extension_loaded('mbstring');

$attachment = new TNEFAttachment();

try {
  $attachment->decodeTnef($buf);
  $tnefFiles = $attachment->getFiles();
} catch (Throwable $e) {
  http_response_code(500);
  exit('TNEF解析でエラー: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'));
}

$files = [];
foreach ((array)$tnefFiles as $f) {
  $name = null;
  $content = null;

  if (is_object($f)) {
    foreach (['getName', 'getFilename', 'getFileName', 'name', 'filename', 'fileName'] as $n) {
      if (method_exists($f, $n)) { $name = $f->$n(); break; }
      if (property_exists($f, $n)) { $name = $f->$n; break; }
    }
    foreach (['getContent', 'getData', 'content', 'data'] as $c) {
      if (method_exists($f, $c)) { $content = $f->$c(); break; }
      if (property_exists($f, $c)) { $content = $f->$c; break; }
    }
  } elseif (is_array($f)) {
    $name = $f['name'] ?? $f['filename'] ?? $f['fileName'] ?? null;
    $content = $f['content'] ?? $f['data'] ?? null;
  }

  if ($content === null || $content === '') continue;

  $files[] = [
    'name' => $name,
    'content' => (string)$content,
  ];
}

$token = safe_token();
$workDir = $TMP_BASE . DIRECTORY_SEPARATOR . $token;

if (!@mkdir($workDir, 0700, true) && !is_dir($workDir)) {
  http_response_code(500);
  exit('一時ディレクトリ作成に失敗しました。');
}

$meta = [
  'created' => time(),
  'source'  => $origName,
  'files'   => [],
];

$usedNames = [];
$idx = 1;

foreach ($files as $f) {
  $rawName = $f['name'] ?? '';
  $name = sanitize_filename($rawName);

  if ($name === 'unknown') {
    $name = 'unknown_' . $idx;
  }

  $name = unique_name($name, $usedNames);
  $usedNames[$name] = true;

  $content = (string)$f['content'];
  file_put_contents($workDir . DIRECTORY_SEPARATOR . $name, $content);

  $meta['files'][] = [
    'name' => $name,
    'size' => strlen($content),
  ];
  $idx++;
}

file_put_contents(
  $workDir . DIRECTORY_SEPARATOR . '_meta.json',
  json_encode($meta, JSON_UNESCAPED_UNICODE)
);

$bodyHtml = find_file_content($workDir, ['message.html', 'message.htm', 'body.html', 'body.htm']);
$bodyTxt  = find_file_content($workDir, ['message.txt', 'body.txt']);
$bodyRtf  = find_file_content($workDir, ['message.rtf', 'body.rtf']);

if ($bodyHtml === null) $bodyHtml = find_first_by_ext($workDir, 'html');
if ($bodyTxt  === null) $bodyTxt  = find_first_by_ext($workDir, 'txt');
if ($bodyRtf  === null) $bodyRtf  = find_first_by_ext($workDir, 'rtf');

if ($bodyHtml !== null) $bodyHtml = to_utf8($bodyHtml);
if ($bodyTxt  !== null) $bodyTxt  = to_utf8($bodyTxt);
if ($bodyRtf  !== null) $bodyRtf  = to_utf8($bodyRtf);

$bodyType = 'none';
$bodyView = '';

if ($bodyHtml !== null) {
  $bodyType = 'html';
  $bodyView = sanitize_html_for_view($bodyHtml);
} elseif ($bodyTxt !== null) {
  $bodyType = 'text';
  $bodyView = htmlspecialchars($bodyTxt, ENT_QUOTES, 'UTF-8');
} elseif ($bodyRtf !== null) {
  $bodyType = 'rtf';
  $bodyView = htmlspecialchars(rtf_to_text_rough($bodyRtf), ENT_QUOTES, 'UTF-8');
}
?>
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>winmail.dat Viewer 結果</title>
  <style>
    body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;max-width:980px;margin:40px auto;padding:0 16px}
    .box{border:1px solid #ddd;border-radius:14px;padding:18px;margin:14px 0}
    pre{white-space:pre-wrap;word-break:break-word;background:#fafafa;border:1px solid #eee;border-radius:12px;padding:14px}
    table{width:100%;border-collapse:collapse}
    th,td{border-bottom:1px solid #eee;padding:10px;text-align:left;font-size:14px}
    .pill{display:inline-block;padding:4px 10px;border:1px solid #ddd;border-radius:999px;font-size:12px;color:#444}
    a.btn{display:inline-block;padding:8px 12px;border:1px solid #ddd;border-radius:10px;text-decoration:none;color:#111}
    .muted{color:#666;font-size:13px;line-height:1.6}
    .row{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
    .warn{background:#fff8e1;border:1px solid #ffe0a3;padding:10px 12px;border-radius:12px}
  </style>
</head>
<body>
  <h1>解析結果</h1>

  <?php if ($mbstringWarning): ?>
    <div class="box warn">
      <div class="muted">PHP拡張 <b>mbstring</b> が無効の可能性があります。文字化けが残る場合は有効化を推奨します。</div>
    </div>
  <?php endif; ?>

  <div class="box">
    <div class="row">
      <span class="pill">token: <?= htmlspecialchars($token, ENT_QUOTES, 'UTF-8') ?></span>
      <span class="pill">source: <?= htmlspecialchars($origName, ENT_QUOTES, 'UTF-8') ?></span>
    </div>

    <p class="muted">
      ※抽出データは一時保存しています。一定時間後に削除されます(download.php 側の cleanup 設定)。
    </p>

    <div class="row">
      <a class="btn" href="download.php?token=<?= urlencode($token) ?>&mode=zip">ZIPで一括DL</a>
      <a class="btn" href="index.php">← 戻る</a>
    </div>
  </div>

  <div class="box">
    <h2>本文プレビュー(<?= htmlspecialchars($bodyType, ENT_QUOTES, 'UTF-8') ?>)</h2>

    <?php if ($bodyType === 'none'): ?>
      <div class="warn">
        <div class="muted">
          本文ファイル(message.html / message.txt / message.rtf)が見つかりませんでした。<br>
          添付なし or Outlook固有情報のみの winmail.dat の可能性があります(正常なケースもあります)。
        </div>
      </div>
    <?php else: ?>
      <?php if ($bodyType === 'html'): ?>
        <div class="muted">※危険なタグ/属性を除去して表示します(完全再現ではありません)。</div>
        <div class="box" style="border:none;padding:0">
          <?= $bodyView ?>
        </div>
      <?php else: ?>
        <pre><?= $bodyView ?></pre>
      <?php endif; ?>
    <?php endif; ?>
  </div>

  <div class="box">
    <h2>ファイル一覧(抽出できたもの)</h2>

    <?php if (count($meta['files']) === 0): ?>
      <div class="warn">
        <div class="muted">
          抽出できたファイルはありませんでした。<br>
          (添付なし/Outlook固有情報のみの winmail.dat の可能性があります)
        </div>
      </div>
    <?php else: ?>
      <table>
        <thead>
          <tr><th>名前</th><th>サイズ</th><th>DL</th></tr>
        </thead>
        <tbody>
          <?php foreach ($meta['files'] as $f): ?>
            <tr>
              <td><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></td>
              <td><?= number_format((int)$f['size']) ?> bytes</td>
              <td><a class="btn" href="download.php?token=<?= urlencode($token) ?>&file=<?= urlencode($f['name']) ?>">DL</a></td>
            </tr>
          <?php endforeach; ?>
        </tbody>
      </table>
    <?php endif; ?>
  </div>
</body>
</html>
<?php
function safe_token(): string {
  try { return bin2hex(random_bytes(16)); }
  catch (Throwable $e) { return sha1(uniqid((string)mt_rand(), true)); }
}

function sanitize_filename($name): string {
  if (!is_string($name)) $name = '';
  $name = trim($name);
  $name = str_replace("\0", '', $name);
  $name2 = preg_replace('/[\/\\\\:\*\?"<>\|]/u', '_', $name);
  if (!is_string($name2)) $name2 = '';
  $name = $name2;
  $name = ltrim($name, '.');
  if ($name === '') return 'unknown';

  if (function_exists('mb_strlen') && function_exists('mb_substr')) {
    if (mb_strlen($name, 'UTF-8') > 180) $name = mb_substr($name, 0, 180, 'UTF-8');
  } else {
    if (strlen($name) > 180) $name = substr($name, 0, 180);
  }
  return $name;
}

function unique_name(string $name, array $used): string {
  if (!isset($used[$name])) return $name;
  $ext = '';
  $base = $name;
  $pos = strrpos($name, '.');
  if ($pos !== false) { $base = substr($name, 0, $pos); $ext = substr($name, $pos); }
  $i = 2;
  while (isset($used[$base . '_' . $i . $ext])) $i++;
  return $base . '_' . $i . $ext;
}

function find_file_content(string $dir, array $candidates): ?string {
  foreach ($candidates as $c) {
    $path = $dir . DIRECTORY_SEPARATOR . $c;
    if (is_file($path)) return file_get_contents($path);
  }
  return null;
}
function find_first_by_ext(string $dir, string $ext): ?string {
  $list = glob($dir . DIRECTORY_SEPARATOR . '*.' . $ext) ?: [];
  if (count($list) === 0) return null;
  return file_get_contents($list[0]);
}

function to_utf8(string $s): string {
  if (preg_match('//u', $s) === 1) {
    if (preg_match('/[\x{0080}-\x{009F}]/u', $s)) {
      if (function_exists('mb_convert_encoding')) {
        $bytes = @mb_convert_encoding($s, 'ISO-8859-1', 'UTF-8');
        $fixed = @mb_convert_encoding($bytes, 'UTF-8', 'SJIS-win');
        if (is_string($fixed) && $fixed !== '' && preg_match('//u', $fixed) === 1) return $fixed;
      }
    }
    return $s;
  }

  if (function_exists('mb_detect_encoding') && function_exists('mb_convert_encoding')) {
    $enc = mb_detect_encoding($s, ['SJIS-win','CP932','ISO-2022-JP','EUC-JP','UTF-8'], true);
    if ($enc) {
      $out = @mb_convert_encoding($s, 'UTF-8', $enc);
      if (is_string($out) && $out !== '') return $out;
    }
    $out = @mb_convert_encoding($s, 'UTF-8', 'SJIS-win,CP932,ISO-2022-JP,EUC-JP,UTF-8');
    return is_string($out) ? $out : $s;
  }
  return $s;
}

function sanitize_html_for_view(string $html): string {
  $html = preg_replace('#<(script|style|iframe|object|embed|link|meta)\b[^>]*>.*?</\1>#is', '', $html);
  $html = preg_replace('#<(script|style|iframe|object|embed|link|meta)\b[^>]*/?>#is', '', $html);
  $html = preg_replace('/\son\w+="[^"]*"/i', '', $html);
  $html = preg_replace("/\son\w+='[^']*'/i", '', $html);
  $html = preg_replace('/javascript:/i', '', $html);
  return $html;
}

function rtf_to_text_rough(string $rtf): string {
  $text = preg_replace('/\\\\par[d]?/i', "\n", $rtf);
  $text = preg_replace('/\\\\line/i', "\n", $text);
  $text = preg_replace('/\\\\tab/i', "\t", $text);
  $text = preg_replace('/\{\\\\\*[^{}]*\}/', '', $text);
  $text = preg_replace('/\\\\[a-zA-Z]+\d* ?/','', $text);
  $text = str_replace(['{','}'], '', $text);
  $text = preg_replace_callback("/\\\\'([0-9a-fA-F]{2})/", function($m){
    return chr(hexdec($m));
  }, $text);
  $text = to_utf8($text);
  $text = preg_replace("/\n{3,}/", "\n\n", $text);
  return trim($text);
}

download.php(個別DL / ZIP DL)

ZIPが「not found (expired or invalid)」になる原因は、view.phpとdownload.phpで一時保存先がズレているケースが多いです。
下記は view.phpと同じ _tmp を参照するため、ズレません。

<?php
// download.php
declare(strict_types=1);

$TMP_BASE = __DIR__ . '/_tmp';
$EXPIRE_SECONDS = 60 * 30;

$token = $_GET['token'] ?? '';
if (!preg_match('/\A[a-f0-9]{32}\z/', $token)) {
  http_response_code(400);
  exit('invalid token');
}

$workDir = $TMP_BASE . DIRECTORY_SEPARATOR . $token;
$metaPath = $workDir . DIRECTORY_SEPARATOR . '_meta.json';

if (!is_dir($workDir) || !is_file($metaPath)) {
  http_response_code(404);
  exit('not found (expired or invalid)');
}

cleanup_old($TMP_BASE, $EXPIRE_SECONDS);

$mode = $_GET['mode'] ?? '';
$file = $_GET['file'] ?? '';

if ($mode === 'zip') {
  if (!class_exists('ZipArchive')) {
    http_response_code(500);
    exit('ZipArchive unavailable (PHP zip extension missing)');
  }

  $meta = json_decode((string)file_get_contents($metaPath), true);
  if (!is_array($meta)) {
    http_response_code(500);
    exit('meta read failed');
  }

  $zipPath = tempnam(sys_get_temp_dir(), 'winmail_zip_');
  $zip = new ZipArchive();
  if ($zip->open($zipPath, ZipArchive::OVERWRITE) !== true) {
    http_response_code(500);
    exit('zip create failed');
  }

  foreach (($meta['files'] ?? []) as $f) {
    $name = (string)($f['name'] ?? '');
    if ($name === '') continue;

    $path = $workDir . DIRECTORY_SEPARATOR . $name;
    if (is_file($path)) {
      $zip->addFile($path, $name);
    }
  }

  $zip->close();

  header('Content-Type: application/zip');
  header('Content-Disposition: attachment; filename="winmail_contents.zip"');
  header('Content-Length: ' . filesize($zipPath));
  header('X-Content-Type-Options: nosniff');

  readfile($zipPath);
  @unlink($zipPath);
  exit;
}

if ($file !== '') {
  $file = sanitize_filename_dl($file);
  $path = $workDir . DIRECTORY_SEPARATOR . $file;
  if (!is_file($path)) {
    http_response_code(404);
    exit('file not found');
  }

  header('Content-Type: application/octet-stream');
  header('Content-Disposition: attachment; filename="' . $file . '"');
  header('Content-Length: ' . filesize($path));
  header('X-Content-Type-Options: nosniff');
  readfile($path);
  exit;
}

http_response_code(400);
echo 'no mode/file';

function sanitize_filename_dl(string $name): string {
  $name = trim($name);
  $name = str_replace(["\0"], '', $name);
  $name = preg_replace('/[\/\\\\:\*\?"<>\|]/u', '_', $name);
  $name = ltrim($name, '.');
  return $name === '' ? 'unknown' : $name;
}

function cleanup_old(string $base, int $expire): void {
  if (!is_dir($base)) return;
  $now = time();
  foreach (glob($base . DIRECTORY_SEPARATOR . '*') ?: [] as $dir) {
    if (!is_dir($dir)) continue;
    $meta = $dir . DIRECTORY_SEPARATOR . '_meta.json';
    $t = @filemtime($meta);
    if ($t !== false && ($now - $t) > $expire) {
      rrmdir($dir);
    }
  }
}

function rrmdir(string $dir): void {
  foreach (glob($dir . DIRECTORY_SEPARATOR . '*') ?: [] as $p) {
    if (is_dir($p)) rrmdir($p);
    else @unlink($p);
  }
  @rmdir($dir);
}

Windowsなら「Winmail Opener」でwinmail.datを手早く表示できる

ここまで紹介したOutlook側/Thunderbird側/PHP Webツール以外にも、Windows環境ならWinmail Openerを使うと、winmail.datをローカルで簡単に開いて中身(本文・添付)を取り出せます。

「いまこのPCでとにかく中身を見たい」「サーバーにアップロードせずにローカルで完結したい」という場面で便利です。

Winmail Openerとは

  • Windows向けの軽量ユーティリティ
  • winmail.dat(TNEF)に含まれる本文・添付ファイルを表示/抽出できる
  • 送信者に再送依頼をする前に、受信側で中身確認ができる

公式サイト(公式URL)

公式サイト:https://www.eolsoft.com/freeware/winmail_opener/

閲覧方法・手順(Windows)

  1. Winmail Openerを公式サイトからダウンロードしてインストールします。
  2. メールに添付されたwinmail.datを、いったんWindows上のフォルダ(例:デスクトップ)に保存します。
  3. 保存したwinmail.datを、Winmail Openerで開きます(ダブルクリック、またはドラッグ&ドロップ)。
  4. 一覧に表示された内容から、本文を確認したり、必要な添付ファイルを選んで抽出(保存)します。

winmail.datが頻繁に届く環境では、.datファイルをWinmail Openerと関連付けておくと、ダブルクリックだけで開けるため非常に効率的です。

拡張子「.dat」は他の用途(データファイル)でも使われる拡張子です。ほかのアプリで使っている場合は影響が出ないか確認してください。
ファイル名が「winmail.dat」であることを確認してから開くのが安全です。

注意点(運用・セキュリティ・トラブルシュート)

1) 社外秘が入る可能性が高い

winmail.datは「本文」「添付」が丸ごと入っていることがあります。社内ツールとして運用するなら、次は必須です。

  • ベーシック認証(またはIP制限)
  • HTTPS
  • 一定時間後に削除(本記事のdownload.phpにcleanupあり)

2) 文字化けは“入力データの文字コード”次第

本文がRTFのみの場合、変換精度は「簡易実装」だと限界があります。必要ならRTF→HTML変換を強化する(別ライブラリ導入)と表示品質が上がります。

3) ZIPが動かない場合

  • PHPのzip拡張(ZipArchive)が無効な場合はZIP作成ができません
  • その場合は個別ダウンロードだけで運用するか、サーバー側設定を見直します

4) 「not found (expired or invalid)」の典型原因

  • view.phpとdownload.phpで一時保存先ディレクトリが違う
  • cleanup時間が短すぎる(テスト中に消える)

まとめ(結局どう運用するのが正解?)

  • 根本解決:Outlook側でTNEF(リッチ形式)送信をやめる(HTML/テキストへ)
  • 個人対応:ThunderbirdならTNEF展開アドオンで受信側で解決
  • チーム最適:Webツールを用意し、winmail.datをアップするだけで表示・抽出できる状態にする
  • 今すぐ1台のWindowsで見たい:Winmail Opener(ローカル完結)

「winmail.dat 表示」で検索してこの記事に辿り着いたなら、まずは送信側(Outlook)に一言相談しつつ、運用上はWebツールを用意しておくのが一番ラクです。
特に外部取引先が多い場合は、トラブル対応時間が激減します。

よくある質問(FAQ)

Q. Thunderbird側をHTML表示にすればwinmail.datが消える?

A. 消えるとは限りません。本文が崩れにくくなることはありますが、winmail.dat自体は「TNEFが送られた」結果なので、受信側の表示設定だけでは解決しないケースが多いです。

Q. 添付が無いのにwinmail.datが付くのはなぜ?

A. Outlookが本文装飾や属性情報をTNEFで送っているためです。受信側がTNEFを解釈できないと、winmail.datとして見えてしまいます。

Q. Webツールを公開してもいい?

A. 推奨しません。社外秘が含まれる可能性があるため、最低でもベーシック認証、できればIP制限を推奨します。

 
※参考にされる場合は自己責任でお願いします。

この記事の運営者(IT部長)からのお知らせ

PCトラブルは解決しましたか?

もし「会社のPCが全部遅い」「Office 365のエラーが多発する」「ネットワークが不安定」といった、調べても解決しない「会社全体」のお悩みがありましたら、ぜひご相談ください。

「Windows11 高速化」といったお悩み検索で毎月1,200人以上が訪れる、
このサイトの運営者
(建設会社IT部長)が、川崎・横浜・東京城南エリアの法人様限定で「無料ITお困りごと診断」を行っています。