C#(.NET 6)でOutlook受信メールの添付(.zip)を自動保存する方法|MailKit IMAPログイン失敗(LOGIN failed)とOAuth2対策

C#(.NET 6)でOutlookの受信メールを読み取り、特定の差出人だけを抽出して、添付(.zip)をローカルへ自動保存したい――。ところがMailKit(ImapClient)のログイン(Authenticate)で止まってしまう。原因になりやすいポイントと、動く実装の作り方を整理します。

目次

やりたいことを「メール処理の4ステップ」に分解する

要件はシンプルに見えても、実装で詰まりやすいのは「接続」と「認証」です。まずは全体像を4ステップに分解して、どこで落ちているのかを切り分けられる状態にします。

ステップやること詰まりやすい点対策の方向性
接続IMAPサーバへTLS接続ホスト名・ポートの不整合IMAPは993(SSL/TLS)を使う
認証ログイン(Authenticate)Basic認証(ユーザー名+パスワード)が弾かれるOAuth2(Modern Auth)でXOAUTH2を使う
検索未読/最新N件を取り出す全件走査で重い・取りこぼすNotSeen+送信者条件+上限設定
保存.zip添付だけローカル保存ファイル名、重複、セキュリティ拡張子判定+安全なファイル名+重複回避

まず直すべき:ImapClientなのに995を指定している問題

結論から言うと、IMAPで読むならポートは993(IMAP over SSL/TLS)です。MailKitのImapClientを使っているのに、995(POP over SSL/TLS)を指定していると、プロトコルが噛み合わず認証以前に破綻します。

プロトコル用途代表ポート暗号化メモ
IMAP受信(サーバ上のメールを同期)993SSL/TLSMailKitならImapClient
POP受信(取得してローカルへ)995SSL/TLSMailKitならPop3Client
SMTP送信587STARTTLSMailKitならSmtpClient

つまり、今回の「Authenticateで失敗」以前に、接続設定がIMAPとPOPで食い違っている可能性が高いです。IMAPで読みたいなら、ホスト名・ポート・暗号化をIMAP用に揃えます。

Microsoft公式:Outlook.comのPOP/IMAP/SMTP設定はこうなっている

Outlook.com(@outlook.com / @hotmail.com / @live.com等)の手動設定は、Microsoftサポートに明記されています。ポイントは次の3つです。

  • POP/IMAPは既定で無効なので、Webの設定で有効化が必要
  • IMAPはoutlook.office365.com:993(SSL/TLS)
  • 認証方式はOAuth2(Modern Auth)が前提
項目値(Outlook.com)補足
IMAPサーバoutlook.office365.com受信(IMAP)
IMAPポート993SSL/TLS
POPサーバoutlook.office365.com受信(POP)
POPポート995SSL/TLS
SMTPサーバsmtp-mail.outlook.com送信
SMTPポート587STARTTLS
認証方式OAuth2/Modern Authユーザー名+パスワードだけだと失敗しやすい

POP/IMAPが無効なら、認証以前に成功しない

Outlook.comはPOP/IMAPアクセスが既定で無効の場合があるため、Outlook.com(Web)で次を確認します。

  • 設定 → メール →(転送とIMAPの設定)
  • 「IMAPを許可」または「POPを許可」をオン
  • 保存

「接続エラー」が出る場合の追加チェック

Microsoftサポートには、IMAPを複数クライアントで設定していると接続エラーになることがあり、Recent activityページで「This was me(これが自分)」を選んで許可する回避策が載っています。認証が急に通らなくなったときに効くケースがあります。

async/awaitの落とし穴:async voidをやめて「必ずawaitする」

メール取得や添付保存はI/Oが多いため非同期化が基本です。ただし、async voidは例外(イベントハンドラ等)を除いて避けるのが定石です。async voidの中で例外が起きると呼び出し元が捕捉できず、結果として「どこで失敗しているか分からない」状態になります。

対策は単純で、

  • 添付保存のメソッドはasync Taskを返す
  • ConnectAsync / AuthenticateAsync / OpenAsync / SearchAsync / GetMessageAsyncなどは必ずawait

という形に揃えます。

なぜ「LOGIN failed」が起きるのか:Microsoft側の仕様変化を押さえる

「メールアドレスとパスワードは合っているのにLOGIN failed」という場合、実はコードよりも認証方式(Basic→Modern)が原因であることが多いです。

Outlook.comはModern Auth(OAuth)へ移行が進んでいる

MicrosoftはOutlook.comでBasic認証を段階的に廃止し、OAuth(Modern authentication)へ移行する方針を明確にしています。Basic認証で接続しようとすると、パスワードの再入力ループや接続失敗になりやすい、という状況が公式に案内されています。

Exchange Online(Microsoft 365)はBasic認証が既に廃止済み

組織アカウント(Microsoft 365 / Exchange Online)の場合はさらに厳しく、IMAP/POPを含む各種プロトコルでBasic認証が利用できないことがMicrosoft Learnに明記されています。加えて、Basic認証に依存する「アプリ パスワード」も使えない方向になるため、新規開発はOAuth2前提で組むのが安全です。

(参考)SMTPでもBasic認証が完全停止へ

受信(IMAP)だけでなく、送信(SMTP AUTH)についてもExchange OnlineではBasic認証のサポート終了が進んでおり、2026年3月〜4月にかけて段階的に拒否され、最終的にBasic認証が完全に使えなくなるとExchange Teamが告知しています。メール処理を一括で自動化するなら、送信側も含めてModern Authへ寄せる設計が重要です。

実装方針:MailKitで読み続けるなら「OAuth2(XOAUTH2)」が現実解

MailKitはOAuth2(SASL XOAUTH2)に対応しています。Microsoft Learnでは、IMAP/POP/SMTPでOAuthを使う際のスコープ例や、XOAUTH2のフォーマット、offline_accessの扱いが説明されています。

重要なポイントだけ抜き出すと次の通りです。

  • IMAPの委任(ユーザー)スコープ例:https://outlook.office.com/IMAP.AccessAsUser.All
  • 必要に応じてoffline_accessを要求すると、更新トークン(refresh token)を使った継続運用がしやすい
  • IMAPサーバへの認証はSASL XOAUTH2(ユーザー名+アクセストークンの組)で行う

サンプル実装:未読メールから「特定送信元の.zip添付」をローカル保存(.NET 6 / MailKit)

ここでは、認証方式の説明に引きずられすぎないよう、メール処理として必要な最小構成を示します。OAuth2でのトークン取得は後段で解説し、この節では「取得済みのアクセストークンを使ってIMAPへ接続し、.zipを保存する」部分に集中します。

前提パッケージ

  • MailKit(MimeKit含む)

処理本体(アクセストークンを受け取って実行する形)

using MailKit;
using MailKit.Net.Imap;
using MailKit.Search;
using MailKit.Security;
using MimeKit;
using System.Globalization;

public static class OutlookZipAttachmentDownloader
{
public static async Task DownloadZipAttachmentsAsync(
string imapHost,
int imapPort,
string mailboxAddress,
string accessToken,
string targetFromAddress,
string saveDirectory,
int maxMessagesToProcess = 50,
CancellationToken cancellationToken = default)
{
Directory.CreateDirectory(saveDirectory);


    using var client = new ImapClient();

    // ※本番で証明書検証を無効化しないこと(デバッグ時のみ検討)
    // client.ServerCertificateValidationCallback = (s, c, h, e) => true;

    // Outlook.com / Microsoft 365 のIMAPは 993 + SSL/TLS(SslOnConnect)
    await client.ConnectAsync(imapHost, imapPort, SecureSocketOptions.SslOnConnect, cancellationToken);

    // OAuth2(XOAUTH2)で認証
    var oauth2 = new SaslMechanismOAuth2(mailboxAddress, accessToken);
    await client.AuthenticateAsync(oauth2, cancellationToken);

    var inbox = client.Inbox;
    await inbox.OpenAsync(FolderAccess.ReadWrite, cancellationToken);

    // 未読 + 送信者(ヘッダFrom)で絞り込み(サーバ検索)
    var query = SearchQuery.NotSeen.And(SearchQuery.FromContains(targetFromAddress));
    var uids = await inbox.SearchAsync(query, cancellationToken);

    // 取りすぎ防止(新しいものから処理したいので後ろから)
    var toProcess = uids.TakeLast(Math.Min(maxMessagesToProcess, uids.Count)).ToList();

    int savedCount = 0;

    foreach (var uid in toProcess)
    {
        var message = await inbox.GetMessageAsync(uid, cancellationToken);

        // 念のため、Fromを厳密に確認(表示名等を排除)
        var fromMatched = message.From.Mailboxes.Any(m =>
            string.Equals(m.Address, targetFromAddress, StringComparison.OrdinalIgnoreCase));

        if (!fromMatched)
            continue;

        foreach (var attachment in message.Attachments)
        {
            if (attachment is not MimePart part)
                continue;

            var fileName = part.FileName ?? string.Empty;

            // .zipのみ対象(大文字小文字は無視)
            if (!fileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
                continue;

            // ファイル名を安全化(パストラバーサル対策)
            var safeName = MakeSafeFileName(fileName);

            // 重複回避:日時+UIDなどを付与
            var prefix = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
            var finalName = $"{prefix}_uid{uid.Id}_{safeName}";
            var fullPath = Path.Combine(saveDirectory, finalName);

            await using var stream = File.Create(fullPath);
            await part.Content.DecodeToAsync(stream, cancellationToken);

            savedCount++;
        }

        // 再処理防止:既読フラグを付ける(運用方針に合わせて変更可)
        await inbox.AddFlagsAsync(uid, MessageFlags.Seen, true, cancellationToken);
    }

    await client.DisconnectAsync(true, cancellationToken);

    return savedCount;
}

private static string MakeSafeFileName(string fileName)
{
    var invalid = Path.GetInvalidFileNameChars();
    var cleaned = new string(fileName.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray());

    // 余計なパス要素を落とす(念のため)
    cleaned = cleaned.Replace("/", "_").Replace("\\", "_");

    // 空なら適当な名前
    if (string.IsNullOrWhiteSpace(cleaned))
        cleaned = "attachment.zip";

    return cleaned;
}


} 

このコードで押さえているポイントは次の通りです。

  • IMAPは993 + SSL/TLS(SslOnConnect)で接続
  • 認証はSaslMechanismOAuth2でアクセストークンを渡す(ユーザー名+パスワードではない)
  • SearchQuery.NotSeenとFromContainsでサーバ側検索し、全件走査を避ける
  • 添付はmessage.Attachmentsを列挙し、MimePartならDecodeToAsyncでファイルへ書き出す
  • 既読フラグを付けて二重取得を防ぐ(要件により「移動」や「Message-Id保存」に変更)

OAuth2アクセストークンの取り方(委任:ユーザーとしてアクセス)

Outlook.com / Microsoft 365 のIMAPにOAuth2で入る場合、Microsoft LearnではIMAPのスコープ例として https://outlook.office.com/IMAP.AccessAsUser.Allを挙げています。長期運用するならoffline_accessも合わせて要求し、更新トークンでアクセストークンを更新できるようにするのが定石です。

コンソールアプリやデスクトップアプリでの現実的な手段としては、MSAL(Microsoft Authentication Library)を使った以下のいずれかが多いです。

  • デバイスコードフロー(コンソールでも扱いやすい)
  • 認可コードフロー(リダイレクトURIを持つアプリ向き)

どちらを使うにせよ「Microsoft Entra(Azure AD)でアプリ登録 → トークン取得 → MailKitにトークンを渡す」という流れになります。

デバイスコードフローのイメージ(概念コード)

実際の運用では、クライアントIDやリダイレクトURI、トークンキャッシュの保存先(ユーザープロファイル等)を整理してください。ここでは「トークンを取ってMailKitへ渡す」最短の形を示します。

// NuGet: Microsoft.Identity.Client
using Microsoft.Identity.Client;

static async Task AcquireImapAccessTokenAsync(
string clientId,
string tenantIdOrCommon,
string mailboxAddress,
CancellationToken ct)
{
// Outlook.com個人アカウントも想定するなら、テナントは common / consumers 等を検討
var app = PublicClientApplicationBuilder.Create(clientId)
.WithAuthority($"[https://login.microsoftonline.com/{tenantIdOrCommon}](https://login.microsoftonline.com/{tenantIdOrCommon})")
.WithDefaultRedirectUri()
.Build();


// Microsoft Learnの例:IMAPの委任スコープ + offline_access
var scopes = new[]
{
    "https://outlook.office.com/IMAP.AccessAsUser.All",
    "offline_access"
};

// まずはサイレント取得を試し、無理ならデバイスコードへ
var accounts = await app.GetAccountsAsync();
try
{
    var result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
        .ExecuteAsync(ct);

    return result.AccessToken;
}
catch (MsalUiRequiredException)
{
    var result = await app.AcquireTokenWithDeviceCode(scopes, code =>
    {
        Console.WriteLine(code.Message);
        return Task.CompletedTask;
    }).ExecuteAsync(ct);

    return result.AccessToken;
}


} 

取得したAccessTokenを先ほどのDownloadZipAttachmentsAsyncに渡せば、IMAP接続→認証→検索→保存が一連で動くようになります。

バックグラウンドで動かしたい場合(非対話):クライアント資格情報フローという選択肢

「ユーザー操作なしで定期実行したい」「サーバーで夜間バッチとして回したい」という場合、委任(ユーザーとしてのログイン)だけだと運用が苦しくなります。Microsoft Learnでは、SMTP/POP/IMAPでクライアント資格情報(client credentials)を使う構成も解説しています。

ただし、これは“何でも簡単にできる魔法”ではなく、少なくとも次が必要です。

  • Microsoft Entraアプリにアプリケーション権限(例:IMAP.AccessAsApp)を付与
  • テナント管理者による管理者同意(admin consent)
  • Exchange Online PowerShellでサービスプリンシパルを登録し、対象メールボックスへ権限付与

権限名の例として、Microsoft Learnには次が挙げられています。

  • POP:POP.AccessAsApp
  • IMAP:IMAP.AccessAsApp
  • SMTP:SMTP.SendAsApp

さらに、Exchange Online側でのサービスプリンシパル登録とメールボックス権限の付与例も示されています(New-ServicePrincipal、Add-MailboxPermissionなど)。「Object IDの取り違えで認証失敗する」注意点まで書かれているため、非対話でやるならこのドキュメントの流れに沿うのが安全です。

LOGIN failed が続くときのチェックリスト(原因→症状→対策)

最後に、現場で多い“ハマりどころ”を原因別に整理します。ログの見方が定まるだけで、復旧速度が一気に上がります。

原因よくある症状対策根拠/補足
IMAPなのに995を使っている接続後すぐ落ちる/認証が進まないIMAPは993に修正(SSL/TLS)Outlook.comのIMAPポートは993
POP/IMAPが無効正しい設定でも弾かれるOutlook.com設定でPOP/IMAPを有効化既定で無効の場合あり
Basic認証(パスワード)で入ろうとしているLOGIN failed / 認証プロンプト地獄OAuth2(XOAUTH2)に切替Outlook.comはOAuthへ移行、Exchange OnlineはBasic廃止
複数IMAPクライアント設定による接続エラー急に接続エラーになるaccount.live.com/activityで接続を承認(This was me)Microsoftが回避策を提示
async void / await漏れ例外が握りつぶされ、原因不明に見えるメソッドはasync Task、呼び出し側でawait非同期I/Oの基本

Microsoft Graphへ切り替えるのはアリ?(結論:長期運用ならかなり有力)

MailKit+IMAPで実現できる一方で、MicrosoftはExchange OnlineのBasic認証廃止に伴い、既存のプロトコルを使い続けるならOAuth2へ更新するか、より新しいプロトコル(Graph API)へ移行することを推奨しています。設計段階から「添付ファイル取得の自動化」を長期運用するなら、Graphも検討対象に入れる価値は高いです。

運用で失敗しないための実務ポイント(オリジナルの現場Tips)

  • 二重取得対策:既読フラグ(Seen)だけに頼らず、Message-IdやUIDをローカルDB/ファイルに記録すると堅い(再実行・障害復旧時に効く)
  • 保存先の権限:Windowsサービスやタスクスケジューラで動かす場合、書き込み権限を事前に確認(Program Files配下は避ける)
  • 添付ファイル名の安全化:相手が付けたファイル名をそのままパスにしない(パストラバーサル対策)
  • ウイルス/マルウェア対策:zipは中身が実行ファイルやスクリプトのこともある。保存後のスキャンや隔離フォルダ運用を検討
  • ログ設計:認証失敗時に「ホスト/ポート/認証方式/スコープ」を必ず残す。トークン本体はログに出さない

参考リンク(公式)

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次