← ALL PATCH NOTES

Gmail send, end to end

v0.9.2 NewBuff
  • Connect Gmail in two clicks. Sequences and one-off sends go out from your own address, and Settings is now your account home.
  • Legion moved to its own surface under Agents, and Origin came back as a compose page.
THE BUILD LOG

The connect button returned JSON, and the expiry column held a duration

Settings showed a Gmail connection that connected to nothing, the authorisation flow never went anywhere, and a column meant to hold a moment in time was storing how long something lasted.

On 2026-06-12 at 10:39, a single commit made Gmail sending work. It is 991 lines, more than half of them tests, and it is mostly a demolition report.

Before it, the Settings page displayed a Gmail row. The row had a status. It looked like an integration. It was hardcoded, and it connected to nothing at all.

Below that, the button that was supposed to start the authorisation flow returned JSON. Not a redirect to Google. A JSON response, to a browser, which does what browsers do with unexpected JSON: nothing useful. The commit body describes the previous state as a dead end, which is generous, because a dead end implies you can at least walk down it.

And the older rows in the database had the number 3599 sitting in a column meant to hold a date.

What 3599 is

When an authorisation provider issues a credential, it tells you how long that credential is good for. It does this with a field called expires_in, whose value is a number of seconds. For Google, that number is 3599, one second short of an hour.

The earlier callbacks had taken that value and written it straight into a column meant to hold the moment the credential dies, not how long it has to live. So those rows claimed to expire 3,599 seconds after the beginning of time.

This is the single most common mistake in the entire category, and it happens because both fields are called something like "expiry" and one of them is a duration wearing a timestamp's name. The code that wrote it was not careless. It read a field, it stored the field, and the field's name did not carry its units.

The repair is the interesting bit. The obvious approach is a migration that finds every bad row and computes the correct value. You cannot, because the information is gone: 3599 tells you it lasted an hour, but not which hour.

So the fix does something better. Anything that cannot be read as a date is treated as already stale, which routes that row through the refresh path the next time it is used, and the refresh path quietly writes a correct value. No migration, no downtime, no guessing. The bad data heals on contact.

I think that is the correct instinct whenever you inherit a corrupted column. Ask what the safest possible interpretation is, and if the safe interpretation triggers a path that produces correct data anyway, you are done.

Three copies of the same sender

The other thing this commit found is that there were three separate implementations of "send an email through a connected provider." One in the main application, one in the bulk outreach path, and one inside the sequence worker.

Three copies is not a style problem, it is a correctness problem with a delay fuse. It means every future improvement has to be made three times by someone who remembers there are three, and the copy that gets forgotten is always the one running unattended in a background worker at two in the morning, which is precisely the one nobody is watching.

This commit collapses them into one service. That service is the first thing in this codebase to live in a services directory, which did not exist until this commit created it as an empty file. The first piece of this application to earn a service layer was not the CRM, or the pipeline, or the agents. It was token refresh, because token refresh is the one thing that must behave identically whether a human clicked a button or a daemon woke up.

That consolidation paid off badly and instructively about six weeks later, when compliance rules arrived and it turned out one of those three senders had been shipping without any of the tracking, logging or unsubscribe machinery the others had. Had the merge not happened first, the compliant sender would have shipped as a fourth copy.

The commit also deletes a window.close() script, which is the fossil of an abandoned approach where authorisation happened in a popup window that closed itself when finished. That code had been sitting there, dead, waiting for a popup that was never opened.

The verification line at the bottom is the part that matters most: connect, send a real email through Gmail, force the token to expire, send again and watch it refresh. Done by hand, by the person who would be embarrassed if it did not work, against a real database branch.

There is no unit test that proves an authorisation flow works. You can test that your code produces the right URL, that it stores what it is given, that it retries correctly. All of that can pass while the actual integration is broken, because the parts you cannot test are the parts where you and Google disagree about what the protocol means. At some point somebody has to click the button and read the email.

The row in Settings is real now. It shows accounts that exist, with a status that comes from the database, and a disconnect button that revokes upstream on a best-effort basis and says so.

Before this commit, that page was a picture of a feature. The gap between a picture of a feature and a feature is 991 lines, and about half of it is proving you did not just draw a better picture.

— DealArena Team