Ozakboy.Gmail 2.1.0

dotnet add package Ozakboy.Gmail --version 2.1.0
                    
NuGet\Install-Package Ozakboy.Gmail -Version 2.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Ozakboy.Gmail" Version="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Ozakboy.Gmail" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Ozakboy.Gmail" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Ozakboy.Gmail --version 2.1.0
                    
#r "nuget: Ozakboy.Gmail, 2.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Ozakboy.Gmail@2.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Ozakboy.Gmail&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Ozakboy.Gmail&version=2.1.0
                    
Install as a Cake Tool

<img src="https://raw.githubusercontent.com/ozakboy/ozakboy.Gmail/main/logo.png" width="112" align="right" alt="Ozakboy.Gmail" />

Ozakboy.Gmail

Gmail REST and Google OAuth in one thin async client. You keep the tokens; it keeps the HTTP.

English | 繁體中文 · Changelog · API Reference · NuGet

A server that reads a support mailbox every five minutes, labels what it finds, trashes the ads and sends the odd reply. That job needs the Gmail API and a refresh token you store yourself — encrypted, per tenant, on your terms. It does not need the whole Google.Apis authentication stack deciding where tokens live. Ozakboy.Gmail is the client for that job: one HttpClient, one delegate that hands over an access token, and the Gmail REST surface you actually use.

🔑  · · ·  ✉

Read a mailbox

using Ozakboy.Gmail;

IGmailClient gmail = new GmailClient(httpClient, ct => Task.FromResult(accessToken));

GmailProfile profile = await gmail.GetProfileAsync();
GmailMessageList page = await gmail.ListMessagesAsync("newer_than:30d", maxResults: 100);

foreach (GmailMessageRef reference in page.Messages)
{
    GmailMessage message = await gmail.GetMessageAsync(
        reference.Id!,
        GmailMessageFormat.Metadata,
        new[] { "From", "Subject", "List-Unsubscribe" });

    Console.WriteLine($"{message.GetHeader("From")}  {message.GetHeader("Subject")}");
}

Label it, trash it, or reply:

await gmail.BatchModifyLabelsAsync(adIds, addLabelIds: new[] { adsLabelId }, removeLabelIds: null);
await gmail.TrashAsync(messageId);                       // recoverable for 30 days — there is no permanent delete here

var reply = new GmailOutgoingMessage { Subject = "Re: Quotation", HtmlBody = "<p>Attached.</p>", InReplyTo = originalMessageId };
reply.To.Add(new GmailAddress("customer@example.com", "Customer"));
reply.Attachments.Add(new GmailAttachmentContent { FileName = "quote.pdf", ContentType = "application/pdf", Content = pdfBytes });

GmailMessage sent = await gmail.SendAsync(reply, threadId: originalThreadId);

And decide what to do when Google says no:

try
{
    GmailHistoryList history = await gmail.ListHistoryAsync(storedHistoryId);
    storedHistoryId = history.HistoryId!;
}
catch (GmailApiException ex) when (ex.IsHistoryExpired) { /* rescan by time window */ }
catch (GmailApiException ex) when (ex.IsUnauthorized)   { /* ask the user to re-authorize */ }

Connect a mailbox

using Ozakboy.Gmail.OAuth;

IGoogleOAuthClient oauth = new GoogleOAuthClient(httpClient, GoogleOAuthOptions.FromConfiguration(configuration));

// 1. send the user here
string url = oauth.BuildAuthorizationUrl(redirectUri, new[] { GmailScopes.GmailModify, GmailScopes.OpenId, GmailScopes.Email }, state);

// 2. on the callback
GoogleTokenResponse token = await oauth.ExchangeCodeAsync(code, redirectUri);
var who = GoogleIdTokenPayload.Parse(token.IdToken!);         // who.Subject, who.Email
// store token.RefreshToken (encrypted), token.AccessToken, token.ExpiresAt

// 3. later: a provider that caches, refreshes ahead of expiry and tells you when to persist
var provider = new GoogleAccessTokenProvider(oauth, refreshToken, new GoogleAccessTokenProviderOptions
{
    OnRefreshed = (fresh, ct) => store.SaveAsync(fresh.AccessToken!, fresh.ExpiresAt, ct),
});
IGmailClient gmail = new GmailClient(httpClient, provider.GetAccessTokenAsync);

What this is

A deliberately thin layer over two Google HTTP APIs:

  • GmailClient — profile, list / get messages (metadata, full, or the raw RFC 822 bytes), history.list for incremental sync, batch get for backfills, threads, modify / batch-modify labels, trash / untrash, report spam, labels CRUD, attachments, and messages.send — with a built-in RFC 822 writer (GmailOutgoingMessage) or your own bytes (SendRawAsync)
  • GoogleOAuthClient — authorization URL, code exchange, refresh, revoke, and reading the id_token claims; GoogleAccessTokenProvider keeps one mailbox's access token cached and refreshed for you
  • GmailApiException — every non-2xx response, with Google's error reason and four flags a sync loop needs: IsUnauthorized, IsHistoryExpired, IsRateLimited, IsNotFound

And what it is not: it does not store or encrypt tokens, does not watch Pub/Sub, does not delete permanently, does not speak IMAP or SMTP, does not cover other Google APIs, and does not classify mail. Those live in your application.

Why it is different

No Google.Apis, no opinions about tokens. The client asks a Func<CancellationToken, Task<string>> for an access token before each request and never sees a refresh token. Encrypt them with your own key, scope them per tenant, rotate them on your schedule — the library has no cache to fight.

gmail.modify is the only scope you need. Sending goes through REST messages.send (the built-in writer produces the RFC 822 message, the client uploads it as message/rfc822), not SMTP. SMTP and IMAP with OAuth require the full https://mail.google.com/ scope; this library never asks for it.

Errors you can branch on. Gmail's per-user quota comes back as HTTP 403, an expired startHistoryId as 404, a dead refresh token as 400 invalid_grant. You do not parse any of that: 429, 5xx and the rate-limit 403s are retried with exponential backoff (three times from one second, Retry-After honoured), and what still fails arrives as one exception type with the right flag set.

No MIME library either. Since 2.0.0 the package depends on nothing beyond the two Microsoft.Extensions.Configuration binding packages (and System.Text.Json on .NET Standard). GmailOutgoingMessage writes text + HTML bodies, attachments, Reply-To, Bcc, threading and custom headers itself; anything fancier, build with MimeKit in your own project and call SendRawAsync.

Async all the way down. No synchronous API, ConfigureAwait(false) on every await, OperationCanceledException never wrapped, models are plain classes with the Gmail REST field names.

Install

dotnet add package Ozakboy.Gmail

Then follow Getting Started — it walks through the Google Cloud setup (Gmail API, consent screen, OAuth client), the authorization flow, a token provider that refreshes itself, and a first sync loop.

✉  · · ·  🔑

Compatibility

Target framework Supported
.NET 10.0 ✅
.NET 9.0 ✅
.NET 8.0 ✅
.NET Standard 2.1 ✅
.NET Standard 2.0 ✅

.NET Standard 2.0 covers .NET Framework 4.6.1+, .NET Core 2.0+ and Mono/Xamarin/Unity.

Dependencies: Microsoft.Extensions.Configuration.Abstractions, Microsoft.Extensions.Configuration.Binder, and System.Text.Json on .NET Standard only. No MIME library since 2.0.0 — see the migration guide if you are upgrading from 1.0.0.

Documentation

Getting Started Google Cloud setup, OAuth flow, token provider, first sync
Configuration GoogleOAuthOptions, GoogleAuthorizationUrlOptions, GmailClientOptions, the refresh-token gotchas
API Reference Every public member, parameter, exception flag and null rule
Migration 1.0.0 → 2.0.0
Changelog Version history

License

MIT.

Support

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.0 113 9/3/2026
2.0.0 100 9/3/2026
1.0.0 95 9/3/2026

v2.1.0 — faster backfill, thread endpoints, body / attachment helpers, a ready-made access-token provider, and finer retry control. No breaking changes.

ADDED:
- BatchGetMessagesAsync(ids, format, metadataHeaders) — fetches many messages through the Gmail batch endpoint (multipart/mixed, up to GmailClientOptions.BatchSize = 50 per HTTP request, chunked automatically). Returns GmailBatchGetResult with Messages and per-id Failures (deleted messages, per-item rate limits) instead of failing the whole batch.
- Thread endpoints: GetThreadAsync, ModifyThreadAsync, TrashThreadAsync, UntrashThreadAsync, with the GmailThread model.
- GmailMessage.GetTextBody() / GetHtmlBody() — walk the MIME parts Gmail already split and return the decoded body; GmailMessage.GetAttachments() — every attachment part as GmailAttachmentInfo (part id, file name, MIME type, size, attachment id, Content-ID).
- GoogleAccessTokenProvider — caches the access token, refreshes it ahead of expiry (RefreshSkew, default 2 minutes), serialises concurrent refreshes, and reports each refresh through OnRefreshed so you can persist it. Pass provider.GetAccessTokenAsync to GmailClient.
- GmailApiException.RetryAfter — the Retry-After value of the final failed response, for job-level backoff.
- GmailClientOptions.MaxRetryDelay (default 60 s) — a Retry-After longer than this is not waited for; the exception is thrown immediately with RetryAfter set. GmailClientOptions.RetryOnNetworkErrors (default false) — retry HttpRequestException with the same backoff.

FIXED:
- On .NET Framework, a 204 No Content response (batchModify, labels.delete, revoke) threw NullReferenceException because HttpResponseMessage.Content can be null there. Found by the new net48 test target.

TECHNICAL:
- The test project now also targets net48 and runs every test against the netstandard2.0 build.

Full changelog: https://github.com/ozakboy/ozakboy.Gmail/blob/main/docs/en/changelog.md