Send transactional email from .NET through several providers' free tiers as one sender. Each message goes to a provider with quota left, and a failure at one provider moves the message to the next without sending it twice.
Status: alpha. The API can change until 1.0.
A small project that sends sign-in links, receipts and alerts can stay on free tiers for a long time, but each tier is small and resets on its own clock. Together the permanent free tiers below come to roughly 900 messages a day. FreeTierMail spends them in the right order, keeps a share for the mail that matters most, and routes around a provider that is down.
- Quota-aware routing. Daily and monthly quotas per provider, with their reset times, are counted and reserved before each send. By default the quota that resets soonest is spent first: a daily quota that resets tonight goes before a monthly one that must last three more weeks.
- Failover without double sends. On throttling, a used-up quota, an outage or a problem with
the account (a revoked key, an unverified domain), the next provider is tried. When a request
went out and no answer came back, it is not: the result is
Unknown, unless you opt in. - Critical mail first.
EmailPriority.Criticalmessages try the providers you mark first, and may use a reserve (10 percent of each quota by default) that normal mail cannot touch. - Idempotency. The same
IdempotencyKeywithin 24 hours returns the first result without sending. Resend also gets the key in its own header. - Honest results. Every send returns what happened at each provider it tried.
- Usage.
GetUsageAsync()returns used, limit and reset time per quota, and thefreetiermail.quota.remaininggauge exports the same.
Marketing campaigns, bulk mail, or getting around a provider's limits. Use one account per provider per sending domain. Opening several free accounts at one provider to multiply its quota usually breaks that provider's terms.
Three packages:
FreeTierMail: the mailer, quota tracking,AddFreeTierMail()for dependency injection, and every HTTP provider below. The providers add no dependency, and Native AOT trimming drops the ones you do not register.FreeTierMail.Smtp: any SMTP relay, through MailKit, for a provider without an API or a paid fallback. Separate so only SMTP users download MailKit.FreeTierMail.Testing: a fake provider and the contract tests every provider passes.
| Provider | Namespace | Free tier, checked 2026-09-26 |
|---|---|---|
| Brevo | FreeTierMail.Brevo |
300 a day, shared by marketing and transactional mail (from 2026 reviews; Brevo's page did not load for us) |
| Mailjet | FreeTierMail.Mailjet |
6,000 a month, 200 a day |
| Resend | FreeTierMail.Resend |
3,000 a month, 100 a day |
| Mailgun | FreeTierMail.Mailgun |
100 a day, one custom domain |
| Elastic Email | FreeTierMail.ElasticEmail |
3,000 a month, 100 a day |
| SMTP2GO | FreeTierMail.Smtp2Go |
1,000 a month |
| MailerSend | FreeTierMail.MailerSend |
500 a month, 100 API requests a day |
| Any SMTP relay | FreeTierMail.Smtp (package FreeTierMail.Smtp) |
whatever your relay allows |
Free tiers change without notice. The numbers are configuration, never constants: set Daily and
Monthly to what your plan says. SendGrid has had no free plan since May 2025, so it is not
listed.
dotnet add package FreeTierMail --prerelease
Add FreeTierMail.Smtp only if you send through an SMTP relay.
using FreeTierMail;
using FreeTierMail.Brevo;
using FreeTierMail.Resend;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
var mailer = new FreeTierMailer(
[
new BrevoProvider(http, new BrevoOptions { ApiKey = brevoKey, Daily = 300 }),
new ResendProvider(http, new ResendOptions { ApiKey = resendKey, Daily = 100, Monthly = 3000, PreferForCritical = true }),
]);
var result = await mailer.SendAsync(new EmailMessage(
from: new EmailAddress("links@example.org", "Example"),
to: [new EmailAddress("rider@example.com")],
subject: "Your sign-in link")
{
TextBody = "Open this link to sign in: https://example.org/verify?token=...",
Priority = EmailPriority.Critical,
IdempotencyKey = "sign-in:4f1c",
});
switch (result.Status)
{
case SendStatus.Sent: /* result.Provider accepted it */ break;
case SendStatus.Unknown: /* a provider may have sent it; do not resend on your own */ break;
case SendStatus.Failed: /* nothing went out; result.Attempts says why at each provider */ break;
}Create one FreeTierMailer per set of accounts and keep it: it holds the quota counts. Give the
HttpClient a timeout and no retry handler; the mailer decides retries.
builder.Services.AddFreeTierMail(builder.Configuration.GetSection("FreeTierMail"))
.AddBrevo()
.AddResend()
.AddMailjet();Quotas, keys and the provider order go in configuration. The wiki covers the rest:
| Page | Covers |
|---|---|
| Configuration | The configuration shape, keys, switching providers per environment, a local inbox, Gmail |
| Bounces and complaints | The suppression list and the webhook for each provider |
| Production | DNS (DKIM, DMARC, SPF), a shared quota store for several instances, tracing and metrics |
- API keys are never logged, traced, put in an exception or printed by
ToString. EmailAddressaccepts one bare address and refuses line breaks, so a header cannot be smuggled in through an address, name or subject.- Webhooks are verified before their body is read, with constant-time comparison; a call that fails the check changes nothing. A suppression prints no address.
- Report a vulnerability as SECURITY.md describes, not in a public issue.
MIT.