Skip to content

Latest commit

 

History

43 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FreeTierMail

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.

Why

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.

What it does

  • 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.Critical messages 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 IdempotencyKey within 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 the freetiermail.quota.remaining gauge exports the same.

What it is not for

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.

Providers

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.

Install

dotnet add package FreeTierMail --prerelease

Add FreeTierMail.Smtp only if you send through an SMTP relay.

Send a message

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.

With dependency injection

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

Security

  • API keys are never logged, traced, put in an exception or printed by ToString.
  • EmailAddress accepts 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.

License

MIT.

Releases

Packages

Used by

Contributors

Languages