← Back to the blog

Legacy modernization

Database Mail Is a Dead End

For twenty years, Database Mail was the right answer. A job fails at 2am, sp_send_dbmail tells someone. A permit gets approved, the stored procedure emails the applicant. No service to deploy, no library to reference, no infrastructure. It lived on the box and it worked.

It stopped working because of something that has nothing to do with SQL Server.

The thing that actually broke

Database Mail speaks SMTP with basic authentication. A username and a password, stored in msdb, sent to a relay.

Microsoft has retired SMTP AUTH client submission in Exchange Online. If your mail is in Exchange Online, that path is closed. If you’re still on-prem, you have time rather than a reprieve — everything is moving the same direction.

And here’s what makes this different from most migrations: there is no Database Mail fix. It has no OAuth support and it isn’t getting any. You cannot configure your way out. The component is a dead end, and the only question is what replaces it.

The tempting wrong answer

The fastest fix is an unauthenticated internal SMTP relay. Point Database Mail at it, let it forward to Microsoft, done in an afternoon.

I understand the appeal, and it’s worth being clear about what it is: you’ve built an open relay inside your network so that a legacy component can keep using an authentication scheme that was retired for being unsafe. Anything that can reach port 25 can now send mail as your organization. In a government environment, that’s a finding waiting to happen.

If you genuinely cannot change the calling application, Microsoft’s High Volume Email is the supported version of that idea, and a better place to stand than a relay you built yourself. But it’s a holding position, not a destination.

What Graph actually asks of you

The real replacement is the Microsoft Graph API. The shape of it: register an application in Entra ID, grant it the Mail.Send application permission — not the delegated one, because no user is signed in when a SQL Agent job fires at 2am — get a token, POST to /users/{id}/sendMail.

That’s the tutorial. Here’s the part the tutorial skips.

The permission you just granted is tenant-wide

Application-scoped Mail.Send does not mean “send as the notifications mailbox.” It means send as any mailbox in the tenant. Your service account. Your director. The mayor.

Nothing in the app registration screen tells you this, and nothing stops it. The control is an application access policy, configured in Exchange Online PowerShell, scoping the app to a mail-enabled security group containing only the mailboxes it should send from.

Create the policy before the app ever gets a token, not after. Once an application identity with unrestricted send exists in your tenant, you’re relying on nobody discovering it, which is not a control.

Then stop using a client secret

A client secret is a password with an expiry date, and you now need somewhere to keep it. Wherever you put it, that’s the new weakest point in the system.

A certificate is better. Better still, if the workload runs outside Azure — and mine run on AWS — is workload identity federation: the compute gets a token from its own platform, Entra trusts that token, and there’s no long-lived credential anywhere. It takes an afternoon and removes an entire category of incident.

Where the send actually lives now

This is the real architectural change, and the one worth thinking about rather than rushing.

Database Mail was called from inside a transaction, in T-SQL, and queued the message itself. Graph is an HTTP call to a service outside your network. Making that call synchronously from a stored procedure means your database transaction now depends on a third party’s availability, and a network blip becomes a failed business operation.

So use an outbox. The database writes a row and commits. A worker service reads the table, calls Graph, and marks the row sent or failed with a retry count.

The caller keeps what made Database Mail good — one statement, fire and forget, no knowledge of how mail works. What you gain is everything it never had: exponential backoff, a dead letter path, and a table you can query when someone asks why the notification didn’t arrive.

Four details that will bite you

Attachments over roughly 3 MB won’t go in a sendMail request. Above that you create a draft message and use an upload session — a different code path you have to write before someone attaches a large PDF, not after.

saveToSentItems defaults to true. Every automated notification lands in a real mailbox’s Sent Items. For a system sending thousands of messages, set it false unless somebody asked for the copy.

Graph throttles, per mailbox and per application, and returns 429 with a Retry-After header. Honor it. A retry loop that ignores Retry-After extends your throttling rather than escaping it, and this only surfaces on the day you send a large batch.

Failures are now yours to see. Database Mail wrote its failures to msdb whether you looked or not. A Graph call that fails inside a worker service is invisible unless you logged it. Build the logging into the first version.

What you lose

Being honest about this makes the plan more credible, not less.

You lose sp_send_dbmail from inside a stored procedure. You lose sysmail_faileditems as a free answer to “did it send.” And you gain a service that has to be deployed, monitored, and kept alive — where before you had a feature that came with the database.

That’s a real trade. It’s still the right one, because the alternative is a component that can no longer authenticate to anything and will not be fixed.

Start with the policy

If you’re doing this, do it in this order: application access policy first, credentials second, outbox third, then the code.

Most teams do it backwards. They get sendMail working in a console app in an hour, feel good about it, and ship an application identity that can send as anyone in the organization.

The code was never the hard part.

Found this useful? Share it.

A ready-made LinkedIn post is one click away, or share the link wherever your team talks.

Discuss your project ↗