C# 读取邮箱邮件(Graph)

SKILL开发
开发 未标注权限 许可证不明确
安装这个技能

用 C# 读取本机/企业邮箱邮件,或做成收邮件提醒的托盘程序。当用户说"用 C# 读邮箱""读取 Outlook 邮件""读收件箱""收邮件托盘程序""邮件提醒""新版 Outlook 读不了""office365 邮箱读邮件""Graph API 读邮件""Outlook COM 报 您没有连接""AADSTS50011 / 65001 / 7000218""net48 装 Microsoft.Graph 报 MSB3245""NotifyIcon 托盘图标"时使用。覆盖:三条路线的选型判断(Outlook COM / IMAP / Graph)、net48 下 Graph SDK 的完整编译配方、MSAL 公共客户端登录与令牌缓存、Azure 应用注册必备三项、托盘程序(图标/气泡/轮询/单实例/自启)落地要点、以及无 GUI 会话下验证 WinForm 程序的套路。

一句话选型结论

按资料字段整理

C# 读取邮箱邮件(Graph)是开发类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类开发 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_825f001d/net48-graph-outlook-mail
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

开发
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_825f001d仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验未标注上游最后核验时间 · 数据更新 1791670982
正文体量25.6 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
写代码与改 bug

按需求补代码、定位并修掉报错。

读仓库与生成改动

让模型读懂项目结构,再给出可落地的改动。

跑测试与排查故障

按现象排查问题,补测试用例并复跑。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在开发场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
C# 读取邮箱邮件(Graph)当前 未标注权限 未标注 未标注 未标注 有
技能审查员 浏览器控制 ClawHub 市场 877.9 万 OpenClaw 有
Vercel React 最佳实践 浏览器控制 GitHub 仓库 849.1 万 Codex、Claude、Cursor 有
GitHub 代码协作 联网访问 ClawHub 市场 763.3 万 OpenClaw 有
网页设计指南 联网访问 GitHub 仓库 668.6 万 Codex、Claude、Cursor 有
本体论 只读 ClawHub 市场 663.7 万 OpenClaw 有

SKILL.md 全文

25.6 KB
模型实际读到的说明书原文,来自技能仓库

用 C# 读取邮箱邮件(net48 + Microsoft Graph)

第一步:先判断走哪条路线,不要直接写代码

先做环境探测,路线选错会白做一整天。

用户实际在用的客户端 能否走 Outlook COM
经典 Outlook(OUTLOOK.EXE,Office 2016/2019/365 桌面版) ✅ 可以,但需已配置账户
新版 Outlook for Windows(olk.exe,包 Microsoft.OutlookForWindows_8wekyb3d8bbwe) ❌ 不行。它基于 Web/Graph,不提供 Outlook.Application COM 自动化接口(微软设计限制)
只开网页版 Outlook ❌ 不行

探测命令

# 1. 看进程里有哪个 Outlook
tasklist | grep -iE "outlook|olk"

# 2. 找经典 Outlook 安装位置
ls "/d/Program Files (x86)/Microsoft Office/Office14/OUTLOOK.EXE" 2>/dev/null
ls "/c/Program Files/Microsoft Office/root/Office16/OUTLOOK.EXE" 2>/dev/null

# 3. 关键:有没有邮件数据库(没有 = 从未配置账户,COM 路线必失败)
find /c /d -maxdepth 6 -iname "*.pst" -o -iname "*.ost" 2>/dev/null | head
ls "$APPDATA/Microsoft/Outlook/" 2>/dev/null

# 4. 判断邮箱服务类型(决定 Graph 还是 IMAP)
nslookup -type=MX 用户域名.cn
nslookup autodiscover.用户域名.cn

判据:
- MX 指向 *.mail.protection.outlook.com 或 autodiscover 指向 *.autodiscover.mira.tm.svc.cloud.microsoft → Exchange Online / M365 → 走 Graph API(最优)。
- 有自建 IMAP(imap.域名 能解析)→ 可走 IMAP,需邮箱 + 授权码。
- 经典 Outlook 已配账户 → 走 COM 最省事(无需 Azure 注册)。

COM 路线的典型死结

GetDefaultFolder(olFolderInbox) 抛 COMException: 无法完成该操作。您没有连接。
= 经典 Outlook 从未配置账户(全盘无 .pst/.ost、%APPDATA%\Microsoft\Outlook 为空)。
无法用代码修复,必须让用户在经典 Outlook 里配好账户,或者换 Graph/IMAP。

> 另一个坑:未配账户时 Outlook 会停在「账户设置向导」窗口,app.Quit() 关不掉向导,进程会残留。用 Stop-Process -Force 清理。


路线 A:Microsoft Graph(推荐,适合 M365 企业邮箱)

A1. net48 工程的编译配方(最关键,照抄)

csc.exe / msbuild.exe 在本机被 LOLBin 黑名单拦截,编译一律走 dotnet build。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>          <!-- 控制台用 Exe -->
    <TargetFramework>net48</TargetFramework>
    <LangVersion>7.3</LangVersion>
    <PlatformTarget>x86</PlatformTarget>
    <FrameworkPathOverride>C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8</FrameworkPathOverride>
    <AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages>
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
    <NoWarn>MSB3270;MSB3277;NU1701</NoWarn>
    <UseWindowsForms>true</UseWindowsForms>
    <!-- ★ 下面这行是 0 警告的关键 -->
    <AssemblySearchPaths>$(AssemblySearchPaths);$(MSBuildProjectDirectory)\refs</AssemblySearchPaths>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
  </PropertyGroup>

  <ItemGroup>
    <!-- 排除同目录下其他小工程的源码与 obj,否则报 CS0579 特性重复 -->
    <Compile Remove="tools\**\*.cs" />
    <Compile Remove="tools\**\obj\**" />
    <!-- 注意:别把 tools\bin 也排掉,那是工具自己的输出目录 -->
  </ItemGroup>

  <ItemGroup>
    <!-- ★ netstandard 门面程序集:必须指向工程内 refs\ 副本 -->
    <Reference Include="netstandard"><HintPath>refs\netstandard.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="System.IO"><HintPath>refs\System.IO.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="System.Runtime"><HintPath>refs\System.Runtime.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="System.Runtime.InteropServices.RuntimeInformation"><HintPath>refs\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="System.Security.Cryptography.Algorithms"><HintPath>refs\System.Security.Cryptography.Algorithms.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="System.Security.Cryptography.Encoding"><HintPath>refs\System.Security.Cryptography.Encoding.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="System.Security.Cryptography.Primitives"><HintPath>refs\System.Security.Cryptography.Primitives.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="System.Security.Cryptography.X509Certificates"><HintPath>refs\System.Security.Cryptography.X509Certificates.dll</HintPath><Private>false</Private></Reference>
  </ItemGroup>

  <ItemGroup>
    <!-- ★ 版本必须锁死。只留这两个,不要手工加 System.Text.Json / System.Memory -->
    <PackageReference Include="Microsoft.Graph" Version="4.54.0" />
    <PackageReference Include="Microsoft.Identity.Client" Version="4.61.3" />
  </ItemGroup>
</Project>

准备 refs\ 目录(一次性,之后可复制到新工程):

mkdir -p refs
FA="/c/Program Files (x86)/Reference Assemblies/Microsoft/Framework/.NETFramework/v4.8/Facades"
for f in netstandard System.IO System.Runtime System.Runtime.InteropServices.RuntimeInformation \
         System.Security.Cryptography.Algorithms System.Security.Cryptography.Encoding \
         System.Security.Cryptography.Primitives System.Security.Cryptography.X509Certificates; do
  cp "$FA/$f.dll" refs/
done

编译:

dotnet build YourApp.csproj -c Release --disable-build-servers -v q --nologo

验收:0 警告 0 错误。

A2. 为什么需要 refs\(别浪费时间去试别的)

报错是每个 netstandard 引用一条 MSB3245: 无法解析引用。
根因:FrameworkPathOverride 模式下 targets 会把搜索路径重置,只在 v4.8 根目录找程序集,而门面程序集实际在 Facades\ 子目录里。

试过但失败的方案(不要重复踩):
- ❌ AssemblySearchPaths 加 {TargetFrameworkDirectory}\Facades —— 被 targets 重置。
- ❌ HintPath 直接指 $(FrameworkPathOverride)\Facades\*.dll —— 仍是主引用解析路径问题。
- ❌ 手工 <PackageReference Include="System.Text.Json" /> 等 —— 会因代理下载失败,且本来就是多余的传递依赖。

有效方案:门面 dll 复制到工程本地 refs\ + AssemblySearchPaths 追加该目录 + HintPath 指向。见 A1。

A3. MSAL 认证(AuthService.cs 骨架)

using Microsoft.Identity.Client;

private static readonly string[] Scopes = new[]
{
    "https://graph.microsoft.com/Mail.Read",
    "https://graph.microsoft.com/User.Read",
    "offline_access"
};

var app = PublicClientApplicationBuilder.Create(clientId)
    .WithAuthority("https://login.microsoftonline.com/" + tenantId)  // ★ 见下方「租户填什么」
    .WithRedirectUri("http://localhost")        // ★ 公共客户端本机回环,必须注册进 Azure
    .Build();

★ 租户(authority)填什么 —— 填错必报 AADSTS50059

这是最容易踩且最难自查的坑(2026-09 实测确认)。

应用注册类型 authority 必须填 填 organizations / common 的后果
仅此组织目录中的帐户(单租户,默认且最常见) 具体租户域名或租户 ID,如 naga-mori.cn 或 11489fe9-b541-437e-8243-f948d31675ff ❌ AADSTS50059: No tenant-identifying information found in either the request or implied by any provided credentials
多租户 organizations 可用 正常

怎么拿租户 ID(不需要登录,公开端点):

curl -s "https://login.microsoftonline.com/<你的域名>/v2.0/.well-known/openid-configuration" \
  | grep -o '"issuer":"[^"]*"'
# issuer: https://login.microsoftonline.com/11489fe9-b541-437e-8243-f948d31675ff/v2.0
#                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 这就是租户 ID

自查口诀:报 AADSTS50059 = 应用是单租户但你用了多租户 authority。不要去查重定向 URI 或权限,跟它们无关。

> 实战教训:MailTray 的 README 曾让你「租户填 organizations」,对单租户应用是错的,会一直失败。
> organizations 只对多租户应用成立。

// token 缓存落盘,跨次启动免登录
app.UserTokenCache.SetBeforeAccess(a => {
if (File.Exists(CachePath)) a.TokenCache.DeserializeMsalV3(File.ReadAllBytes(CachePath));
});
app.UserTokenCache.SetAfterAccess(a => {
if (a.HasStateChanged) File.WriteAllBytes(CachePath, a.TokenCache.SerializeMsalV3());
});

// 交互式登录
var r = await app.AcquireTokenInteractive(Scopes)
.WithPrompt(Prompt.SelectAccount)
.WithUseEmbeddedWebView(false) // ★ system browser 模式必须显式关闭内嵌 WebView
.ExecuteAsync();

// 静默刷新
var accounts = await app.GetAccountsAsync();
var silent = await app.AcquireTokenSilent(Scopes, accounts.First()).ExecuteAsync();


缓存路径放 `%LOCALAPPDATA%\<AppName>\token_cache.bin`(**不要放 exe 目录**,随目录移动会丢)。

### A4. 调 Graph 拉邮件(Graph **4.x** API)

```csharp
using Microsoft.Graph;
using Microsoft.Graph.Auth;   // ❌ 这个包不存在,别 using

// 认证走 DelegateAuthenticationProvider 把 token 塞进 header
var authProvider = new DelegateAuthenticationProvider(request => {
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    return Task.CompletedTask;
});
var graph = new GraphServiceClient(authProvider);

var page = await graph.Users[mailbox].MailFolders.Inbox.Messages
    .Request()
    .Top(top)
    .OrderBy("receivedDateTime desc")
    .Select("id,subject,from,receivedDateTime,isRead,bodyPreview,hasAttachments,webLink,body")
    .GetAsync();

foreach (var m in page) {
    string sender = m.From?.EmailAddress?.Name;
    // 附件名需二次查询
    var atts = await graph.Users[mailbox].Messages[m.Id].Attachments
        .Request().Select("name").GetAsync();
}

> ⚠️ 不存在 Microsoft.Graph.Auth 包(旧教程常见误导)。认证统一用 MSAL 或 Azure.Identity。
> ⚠️ 异常属性是 ServiceException.StatusCode,不是 ResponseStatusCode。
> ⚠️ Microsoft.Graph v5+ 的 Kiota 版不支持 .NET Framework,必须锁 4.x。

A5. AADSTS 错误码 → 用户该去哪点哪里

把下表做成 DescribeMsalError(),界面报错时直接告诉用户"去门户哪一步改什么",比让用户查文档强得多。

错误码 含义 修法
AADSTS50011 重定向 URI 不匹配 身份验证 → 添加平台「移动和桌面应用程序」→ 勾 http://localhost
AADSTS65001 / 65004 未授予权限 API 权限 → 点「代表 <租户> 授予管理员同意」
AADSTS7000218 未识别为公共客户端 身份验证 → 高级设置 → 「允许公共客户端流」= 是
AADSTS700016 / 500011 ClientId 或租户不对 核对 ClientId(是否误填了「对象 ID」)
AADSTS50059 单租户应用用了 organizations/common 多租户 authority authority 换成具体租户域名或租户 ID(见 A3 下方专节)
AADSTS50105 / 53003 条件访问策略拦截 账号被限制在此设备/应用登录,需管理员放行
浏览器没弹出 默认浏览器异常 让用户把默认浏览器设为 Edge/Chrome

A6. Azure 应用注册必备三项(缺任一都会失败)

  1. 身份验证 → 添加平台 → 移动和桌面应用程序 → 勾 http://localhost(顺手也加 https://login.microsoftonline.com/common/oauth2/nativeclient)
  2. 身份验证 → 高级设置 → 允许公共客户端流 = 是
  3. API 权限 → Microsoft Graph → 委托的权限(不是「应用程序权限」!)→ Mail.Read + User.Read → 代表 <租户> 授予管理员同意

门户入口:https://entra.microsoft.com(或 portal.azure.com 搜「Microsoft Entra ID」)。
- 账号至少要 应用程序开发者 或 云应用程序管理员 角色。
- 不需要 Azure 订阅、不需要客户端密码(公共客户端不存密码)。

> 企业租户通常关闭了「用户自行同意第三方应用」→ 第 3 步的「授予管理员同意」是必需的,不是可选。

卡在「需要批准」同意页怎么破(2026-09 MailTray 实战)

页面特征:三个权限打勾 + 「输入请求访问此应用的理由」文本框 + 「接受」按钮置灰 + 「此应用是未经认可的吗」
→ 租户开了管理员同意工作流(用户同意被关),用户自己点不了接受。处理顺序:

  1. 理由框直接填一句提交(会转给租户管理员审批)。理由模板:
    「MailTray 为自研桌面托盘小工具,仅用于轮询本人收件箱的新邮件提醒,申请本人邮箱的
    Mail.Read/User.Read 委托权限,不涉及组织数据导出,不保存密码。」
  2. 有管理员角色(全局管理员/应用程序管理员/云应用程序管理员)→ 开一键同意 URL 秒批:
    https://login.microsoftonline.com/&lt;租户域名或ID&gt;/adminconsent?client_id=&lt;ClientId&gt;
  3. ROPC(grant_type=password,纯账密换 token)绕不开这次授予:权限从未被同意过照样
    AADSTS65001;租户强制 MFA 时直接 AADSTS50076 失败;微软已不推荐。别把它当"免审批方案"。

预期管理:审批 + 登录都只需一次——管理员批一次,用户交互登录一次,MSAL 令牌缓存落盘
(offline_access 刷新令牌随用随续)之后托盘轮询永久静默,不会再弹任何授权页。M365 的
账号密码直连(基础认证 IMAP/POP)微软 2022 年起已全面禁用,任何代码都绕不开 OAuth。

★ 别急着找管理员:先按「权限风险分级」试探一次(2026-09 实测有效)

用户说"卡在需要批准"时,不要立刻判定必须走管理员审批。很多租户的策略是
「允许用户同意低风险权限,只拦中高风险」——此时收窄权限就能自助开通。

实测结论(租户 naga-mori.cn,2026-09):

Graph 委托权限 风险级别 该租户实测
User.Read、Mail.Read、Calendars.Read、Contacts.Read 低 ✅ 用户可自行同意,同意页「接受」可点
Mail.Send 中 ❌ 需管理员同意
Mail.ReadWrite 及各类 *.ReadWrite 中/高 ❌ 需管理员同意

操作顺序(成本从低到高,别一上来就发审批邮件):

  1. 先只请求低风险只读权限跑一次 device code 登录。通过 → 只读能力立刻可用,业务先转起来。
  2. 只读被拦或确需写权限 → 才走管理员审批(管理员同意工作流 / adminconsent 链接)。
  3. 给管理员的申请材料里,只列真正需要的那几项,并按风险分级说明,通过率明显更高。

> 判断技巧:把 scope 拆成"低风险只读"和"中高风险"两批分别试。
> 一次性把 Mail.ReadWrite + Mail.Send 全要上,往往直接被拒,还多耗一轮审批。

adminconsent 只能授予「已配置在应用上」的权限

一键同意链接:

https://login.microsoftonline.com/&lt;租户ID&gt;/adminconsent?client_id=&lt;ClientId&gt;

前置条件:该权限必须已经写在 Azure 应用的「API 权限」列表里。
应用上只配了 Mail.Read,那 adminconsent 也只能批 Mail.Read;代码里再请求 Mail.Send
照样失败。所以顺序永远是 先在应用里加权限 → 再授予同意,反过来白跑。


路线 B:IMAP(有自建 IMAP,或不想做 Azure 注册时)

需要:邮箱地址 + 授权码 / 应用密码(不是登录密码)。
Gmail/QQ/163 都要先在网页端「账户安全」里生成授权码。
.NET Framework 下用 MailKit(注意 MailKit 4.x 起要求 net472+,更老版本要挑)。

比 Graph 简单,但拿不到「已读状态/附件元数据」之外的 Office 365 丰富字段,且 M365 现在基本禁用了基础认证 IMAP —— M365 邮箱优先走 Graph。


无 GUI 会话下验证 WinForm 程序(必用套路)

判定程序是否真崩:看 timeout 退出码

# 后台启动 (./App.exe &amp;) 的进程会随 shell 会话结束被回收,
# 看起来像&quot;闪退&quot;,其实不是!
timeout 12 ./App.exe ; echo &quot;exit=$?&quot;
# exit=124 → 被 timeout 杀掉 → 程序一直活着(真崩溃会是别的码)

正式验证用 PowerShell 工具的 Start-Process -PassThru(其子进程不被 shell 会话回收):

$p = Start-Process -FilePath &quot;路径\App.exe&quot; -WorkingDirectory &quot;输出目录&quot; -PassThru
Start-Sleep -Seconds 6
if ($p.HasExited) { &quot;已退出 ExitCode=&quot; + $p.ExitCode }
else { &quot;PID=$($p.Id)&quot; ; &quot;Title=[$($p.MainWindowTitle)]&quot; ; &quot;Handle=$($p.MainWindowHandle)&quot; }

启动日志打点(比猜快得多)

在 Main 里加三处日志:进入 Main → 窗体构造完成 → 进入消息循环。写到 exe 同目录 startup.log。
三行都有 = 界面正常;只有前两行 = 消息循环前崩了;一行都没有 = 启动前就崩。

截窗口图:按 PID 找,不要按标题

❌ FindWindowW(null, &quot;中文标题&quot;)  —— 中文全角括号经命令行传参编码损坏,永远匹配不到
✅ EnumWindows + GetWindowThreadProcessId 取目标 PID 的顶层窗口,
   过滤 IsWindowVisible,取面积最大的那个,再 GetWindowTextW / GetWindowRect / CopyFromScreen

编译成独立 csproj(本机 Add-Type 运行时编译 .NET 代码被安全策略拦截,P/Invoke 探针必须走 dotnet build)。


做成托盘常驻程序(NotifyIcon)的要点

用户要"托盘程序"时,是在 winform 基础上加 NotifyIcon,不是另起炉灶。以下都是实际踩过的。

⚠️ BeginInvoke 时机:必须等句柄创建

Main 里窗体构造完但消息循环尚未进入时,窗口句柄还没创建。此时调
BeginInvoke / Invoke 会抛:

InvalidOperationException: 在创建窗口句柄之前,不能在控件上调用 Invoke 或 BeginInvoke。

要做"显示后立即隐藏/最小化"这类动作,一律挂到 Form.Shown 事件里(那时句柄必定已创建):

// ❌ 错:Main 里直接调
form.BeginInvoke(new Action(() =&gt; form.Hide()));

// ✅ 对:挂在 Shown
form.Shown += async (s, e) =&gt; {
    if (hideOnStart) form.Hide();
    await StartupAsync();
};

托盘图标:运行时动态绘制,不依赖 .ico

免去"图标丢了/打包漏了"。用 GDI+ 画好 → GetHicon → Clone() 后再 DestroyIcon(直接返回 Icon.FromHandle 会泄漏句柄):

IntPtr h = bmp.GetHicon();
try {
    using (var tmp = Icon.FromHandle(h))
        return (Icon)tmp.Clone();      // 必须 Clone,否则 DestroyIcon 后图标失效
} finally {
    DestroyIcon(h);                     // 释放原生句柄
}

图标配色的状态语义(推荐):蓝=正常 / 黄=待登录或未配置 / 灰=检查中 / 红=出错;未读数做成红色数字角标。

NotifyIcon.Text 有 63 字符上限

超长会抛异常(不是截断),必须自己截断:

_icon.Text = s.Length &lt;= 62 ? s : s.Substring(0, 62);

关窗口 ≠ 退出

托盘程序的关闭按钮应当只隐藏。真退出走托盘菜单:

FormClosing += (s, e) =&gt; {
    if (!RealExitRequested &amp;&amp; e.CloseReason == CloseReason.UserClosing) {
        e.Cancel = true;
        Hide();
    }
};
// 托盘菜单「退出」里:form.AllowRealExit(); Application.Exit();

单实例守卫

托盘程序开两份会出现两个图标、状态互相覆盖。用命名 Mutex(Local\ 前缀 = 同一用户会话内有效):

_mutex = new Mutex(true, @&quot;Local\AppName_SingleInstance_XXXX&quot;, out bool createdNew);
if (!createdNew) { /* 已有实例,提示后退出 */ }

Mutex 要放在静态字段里活着到进程结束,否则会被 GC 回收。

开机自启:写 HKCU Run,免管理员

using (var key = Registry.CurrentUser.OpenSubKey(
    @&quot;Software\Microsoft\Windows\CurrentVersion\Run&quot;, true))
{
    key.SetValue(&quot;AppName&quot;, &quot;\&quot;&quot; + Application.ExecutablePath + &quot;\&quot; --minimized&quot;);
}

比创建启动文件夹 .lnk 简单(不依赖 COM)。

首次未配置时不要隐藏窗口

如果默认"启动最小化到托盘",用户首次运行时只看到托盘图标,不知道去哪填 ClientId。
应在 !cfg.IsReady 时强制显示窗口做引导。

⚠️ Windows 11 无法用 P/Invoke 枚举托盘图标

经典路径 Shell_TrayWnd → TrayNotifyWnd → SysPager → ToolbarWindow32
在 Windows 11 的新任务栏里已不存在(SysPager 和 ToolbarWindow32 都返回 0)。
不要浪费时间写这种探针。

验证托盘链路是否正常的正解:写一个独立自验工具,直接跑 NotifyIcon 全链路并写日志:

[1] GDI+ 生成图标 → Icon 转换
[2] NotifyIcon.Visible = true
[3] ShowBalloonTip 调用
[4] 定时动态换图标 + 改文本(多次)
[5] 退出时 Visible = false + Dispose

跑 15–20 秒后退出码 0、日志无 !! 即链路通。


轮询新邮件:增量查询,别每次拉全量

托盘提醒的关键设计。首轮拉全量建立基线,但不弹提醒(否则第一次运行会把整个收件箱当新邮件刷屏);后续轮用时间过滤增量查:

// OData 日期过滤:必须 UTC,且格式不能带毫秒,否则 400
string filter = &quot;receivedDateTime gt &quot; + since.ToUniversalTime().ToString(&quot;yyyy-MM-ddTHH:mm:ssZ&quot;);
var page = await graph.Users[mailbox].MailFolders[&quot;Inbox&quot;].Messages
    .Request().Top(100).OrderBy(&quot;receivedDateTime desc&quot;)
    .Select(&quot;id,subject,from,receivedDateTime,isRead,bodyPreview,hasAttachments,webLink&quot;)
    .Filter(filter)          // 只在增量轮加
    .GetAsync();

三个要点:
- 用 HashSet&lt;string&gt; 按邮件 Id 去重,避免重复推送。
- 时间点往回多退 2 分钟再查,防时钟偏差/服务器延迟导致边界邮件漏掉。
- Graph 的 Filter 与 OrderBy 组合对某些属性有限制,receivedDateTime 是支持排序的。

> 注意:先 Top().OrderBy().Select() 拿到 request 对象再 .Filter(),或反之皆可,但都要在 GetAsync 之前。


用户给了邮箱密码怎么办

不要把它写进程序或配置。 正确做法:

  1. 明确告知:M365 / 企业邮箱已禁用基础认证(微软 2022 起),账号密码直连 IMAP/POP 会失败;
  2. 说明 Graph/OAuth 路线程序不需要也不保存密码(浏览器授权 + 令牌缓存);
  3. 建议用户改密码 + 开 MFA,因为他已经把密码明文发出去了。

对用户的安全性负责,比"照做"更重要。


WinForm 纯代码构建界面:用 TableLayoutPanel,不要靠 Dock 顺序

Controls.Add 的 Dock 叠加顺序极易错(标签被输入框压住、附件区跑顶部)。别靠调顺序试。

var p = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 9, Padding = new Padding(8) };
p.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
p.RowStyles.Add(new RowStyle(SizeType.Absolute, 18F));    // 0 标签
p.RowStyles.Add(new RowStyle(SizeType.Absolute, 46F));    // 1 主题(多行)
p.RowStyles.Add(new RowStyle(SizeType.Absolute, 18F));    // 2 标签
p.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));    // 3 正文(自适应占满剩余)
p.RowStyles.Add(new RowStyle(SizeType.Absolute, 96F));    // 4 附件列表
p.Controls.Add(lblSubj, 0, 0);
p.Controls.Add(_txtSubject, 0, 1);

用 Absolute 放标签与单行控件,用 Percent 100F 放"吃掉剩余空间"的区域。 一次成型。


交付检查清单

  • [ ] dotnet build -c Release --disable-build-servers → 0 警告 0 错误
  • [ ] 运行时依赖齐全(Microsoft.Graph*.dll、Microsoft.Identity.Client.dll、System.Text.Json.dll 等都在输出目录)
  • [ ] 冒烟测试:进程存活 + MainWindowTitle 非空 + MainWindowHandle 非零 + startup.log 三行齐
  • [ ] 托盘程序另加:图标生成 / Visible / 气泡 / 动态更新 / 正常注销 五步自验
  • [ ] 截图确认界面完整渲染(无遮挡、无错位)
  • [ ] 清理测试残留进程与临时文件
  • [ ] 给用户一份 Azure 注册指引(桌面 HTML 或 MD),并在程序里加「打开指引」按钮
  • [ ] 用户若给了密码:不写入任何文件,并提示改密码

```

常见问题

5 问
答案由本站依据条目字段整理
C# 读取邮箱邮件(Graph)怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_825f001d/net48-graph-outlook-mail。安装方式为未标注。装完刷新技能列表或重开客户端。

C# 读取邮箱邮件(Graph)支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

C# 读取邮箱邮件(Graph)权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

C# 读取邮箱邮件(Graph)收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

C# 读取邮箱邮件(Graph)用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 未标注。