Intellivega.GraphMail 1.0.0

Intellivega.GraphMail

Send Microsoft 365 email from .NET 10 services through Microsoft Graph using app-only authentication. Built from Intellivega's BaGet Package Template.

Supports plain text and HTML, To/Cc/Bcc, display names, Reply-To, small file and inline attachments, cancellation, and Sent Items control. Includes dependency injection registration and Azure.Identity token caching. Uses the Graph v1.0 HTTP API directly, without a dependency on the full Microsoft.Graph SDK.

SMTP migration guide ยท Compilable console example

Install

Once version 1.0.0 has been published to the Intellivega feed:

dotnet nuget add source https://baget.intellivega.com/v3/index.json --name Intellivega
dotnet add package Intellivega.GraphMail --version 1.0.0

Configure the source only once. Before feed publication, download the .nupkg from the GitHub Actions artifact and use its directory as a local NuGet source.

Configure

Create an Entra app and authorize it to send from an Exchange Online mailbox. See the migration guide for permissions and mailbox scoping. The package targets the Microsoft public cloud and app-only service workloads.

{
  "GraphEmail": {
    "TenantId": "YOUR-TENANT-ID",
    "ClientId": "YOUR-APPLICATION-ID",
    "Sender": "notifications@example.com"
  }
}

Supply GraphEmail:ClientSecret using a secret store or the environment variable GraphEmail__ClientSecret. Do not put secrets in source-controlled JSON. All four settings also accept the GraphEmail__ environment-variable prefix when the host uses the default .NET configuration providers.

In an ASP.NET Core or worker host:

using Intellivega.GraphMail;

builder.Services.AddGraphEmail(
    builder.Configuration.GetSection(GraphEmailOptions.SectionName));

Missing settings fail host startup. Settings are read when the sender is constructed; restart the host after changing credentials or the sender. Register once per service collection. The returned IHttpClientBuilder allows HTTP timeout customization. The default HttpClient timeout is 100 seconds for the Graph request; pass cancellation to bound the entire call, including token acquisition. Do not add automatic retry handlers to the send client.

For managed identity, certificates, or workload identity, supply an app-only TokenCredential explicitly instead of client-secret configuration:

using Azure.Identity;
using Intellivega.GraphMail;

builder.Services.AddGraphEmail(
    options => options.Sender = "notifications@example.com",
    new ManagedIdentityCredential());

That identity still needs Exchange authorization. This overload requires only Sender; tenant/client/secret options are unused. No interactive user login or /me endpoint is used. Sovereign clouds and delegated-user flows are outside this package's scope.

Send

Inject IGraphEmailSender into your service:

var result = await sender.SendAsync(new GraphEmailMessage
{
    To = [new("customer@example.com", "Customer")],
    Cc = [new("accounts@example.com")],
    ReplyTo = [new("support@example.com", "Support")],
    Subject = "Your invoice",
    Body = "<p>Your invoice is attached.</p>",
    IsHtml = true,
    Attachments =
    [
        new GraphEmailAttachment
        {
            Name = "invoice.pdf",
            ContentType = "application/pdf",
            Content = await File.ReadAllBytesAsync("invoice.pdf", cancellationToken)
        }
    ]
}, cancellationToken);

The configured mailbox is always the sender. Supply bare recipient addresses and optional display names separately. Bcc-only and Cc-only messages are valid. Sent Items saving defaults to true; set SaveToSentItems = false per message to opt out. For inline images, set IsInline = true, ContentId = "logo", and use <img src="cid:logo"> in the HTML body. Do not mutate messages, lists, or byte arrays during a send.

Each attachment must be smaller than 3,000,000 bytes, and the serialized JSON request must be smaller than 4,000,000 bytes, including base64 overhead. These are conservative package limits, not a promise that your tenant accepts every message below them. Larger files require a separate draft/upload-session flow, which this package does not implement. Validation runs before authentication.

Acceptance and failures

A completed call means Graph returned HTTP 202 Accepted, not that delivery completed. GraphEmailSendResult.RequestId is a diagnostic correlation ID, not a message ID or delivery receipt. Use Exchange message trace/NDRs to investigate delivery. See Microsoft's sendMail contract.

Non-202 responses throw GraphEmailException with StatusCode, RequestId, and optional RetryAfter (supports seconds and HTTP-date headers). Response bodies are never copied into exceptions. Authentication exceptions from Azure.Identity, transport exceptions, and cancellation propagate unchanged.

There are no automatic send retries. On 429, let your queue honor RetryAfter; on network failures, timeouts, cancellation after dispatch, or 5xx, the outcome may be unknown. Blind retries or immediate SMTP fallback can send duplicates. Token acquisition may have its own Azure.Identity retries. Applications own queueing, duplicate prevention, delivery monitoring, and retry policy.

DI registration disables HTTP redirects and built-in HttpClient logging to avoid logging mailbox URLs. The package does not log email content or tokens. Review any application-added telemetry and credential exception logging separately. If constructing GraphEmailSender directly, the caller owns the HttpClient and credential, and should disable redirects and automatic POST retries.

Example and development

The console example sends nothing unless invoked with --send <recipient>. Configure GraphEmail__TenantId, GraphEmail__ClientId, GraphEmail__ClientSecret, and GraphEmail__Sender in its environment, then run:

dotnet run --project samples/GraphMail.Console -- --send recipient@example.com

This command sends one real message; choose an approved test recipient.

dotnet build -c Release
dotnet test -c Release --no-build
dotnet pack src/Intellivega.GraphMail -c Release --no-build -o artifacts

Tests use fake credentials and an in-memory HTTP handler. They never contact Microsoft Graph or send email. The solution also builds the console example. GitHub Actions builds, tests, and uploads the NuGet package. The package includes this README and the SMTP migration guide.

Publish

Increment Version before each new release, run the checks above, and publish the exact package from a machine with access to BaGet. In Bash:

read -rsp "BaGet API key: " BAGET_API_KEY; echo
dotnet nuget push artifacts/Intellivega.GraphMail.1.0.0.nupkg \
  --source https://baget.intellivega.com/v3/index.json \
  --api-key "$BAGET_API_KEY"
unset BAGET_API_KEY

Never commit feed credentials. CI creates artifacts; it does not publish to BaGet.

No packages depend on Intellivega.GraphMail.

Version Downloads Last updated
1.0.0 127 09/18/2026