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 | 受信(サーバ上のメールを同期) | 993 | SSL/TLS | MailKitならImapClient |
| POP | 受信(取得してローカルへ) | 995 | SSL/TLS | MailKitならPop3Client |
| SMTP | 送信 | 587 | STARTTLS | MailKitなら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ポート | 993 | SSL/TLS |
| POPサーバ | outlook.office365.com | 受信(POP) |
| POPポート | 995 | SSL/TLS |
| SMTPサーバ | smtp-mail.outlook.com | 送信 |
| SMTPポート | 587 | STARTTLS |
| 認証方式 | 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は中身が実行ファイルやスクリプトのこともある。保存後のスキャンや隔離フォルダ運用を検討
- ログ設計:認証失敗時に「ホスト/ポート/認証方式/スコープ」を必ず残す。トークン本体はログに出さない

コメント