The two-way email sync we deleted instead of building
Testing GoHighLevel first showed its native Gmail two-way sync already met the client's needs, avoiding a custom IMAP/SMTP integration.
The two-way email sync we deleted instead of building
An e-commerce client on GoHighLevel (GHL) wanted email to live where their contacts and conversations already live. The request was reasonable. The scoped solution — a bespoke IMAP/SMTP layer bolted onto the CRM — was not. Before we opened an editor, we opened the platform and checked what it already did.
It did all of it.
The problem
The client ran their business on GHL. They wanted a mailbox connected to the CRM so that incoming and outgoing email showed up against the right contact, in the right conversation, with threads and timestamps intact. Reply from the CRM, send from the CRM, and never lose the trail.
That is a real need. Scattered email is how context goes missing: a customer writes in, a reply goes out from someone's personal inbox, and the CRM never sees it. The goal — email and contacts in one place — was the right goal.
The problem was how the ticket proposed to reach it.
Why the obvious fix didn't work
The obvious fix, and the one the ticket described, was to build the sync ourselves.
On paper it reads like a normal integration project. Connect a mailbox over IMAP. Poll it for new mail. Parse each message and associate it with the right contact and conversation. Prevent duplicates. Handle attachments. Send outbound over SMTP. Preserve threading and timestamps so a conversation reads in order.
Then comes everything that keeps that alive in production: secure credential storage, retry and error logging, sync monitoring, and background jobs to run the polling loop. None of that is exotic. All of it is real, and all of it is forever.
Here is what "just build the sync" actually signs you up to build and maintain:
- An IMAP poller — a background job that connects, checks for new mail on a schedule, and never falls behind.
- Deduplication — logic to make sure the same message doesn't land twice when a poll overlaps or retries.
- Threading and timestamps — reconstructing conversation order so replies don't scramble.
- OAuth and authentication — credentials stored securely, tokens refreshed, provider quirks absorbed.
- Retries and error logging — because IMAP and SMTP fail intermittently, and silent failure is lost mail.
- Sync monitoring — a way to see that it's still working, because an email sync that dies quietly is worse than none.
That is not a feature. That is a small product with an on-call rotation. And every hour spent on it is an hour not spent on the client's actual business. The obvious fix didn't fail because it was hard to build — it failed because it was the wrong thing to build.
What we did
We checked the platform before we wrote the platform.
We researched and tested GHL's own email handling. When a user connects a Gmail mailbox through GHL's built-in two-way sync, the platform already does the entire job the ticket described:
- Incoming and outgoing emails sync into conversations automatically.
- Emails for an assigned contact appear in the inbox against that contact.
- Threading is preserved, so conversations read in order.
- OAuth, authentication, and provider handling are managed by the platform, not by us.
Everything on the ticket — inbound capture, outbound send, contact association, threading, credentials — was already there, maintained by the vendor, running for every customer on the platform.
So we recommended the thing that produces no pull request: use the native two-way Gmail sync. Don't build the custom IMAP/SMTP layer. We judged the custom implementation not needed and said so plainly. A separate custom sync would only add complexity and duplicate functionality the client was already paying for.
Then we deleted the ticket.
How it works
The native path is short, and its shortness is the point.
The user connects their Gmail mailbox to GHL through the platform's built-in two-way sync and authorizes it once via OAuth. From that moment, the platform owns the pipe. Inbound mail flows into the matching contact's conversation. Replies sent from the CRM go out through the same connection and stay on the thread. Threading and timestamps are preserved by the platform. Authentication and provider handling are the vendor's responsibility.
Contrast that with the layer we would have owned instead: a poller reaching into the mailbox over IMAP on a timer, a dedup check on every message, a threading reconstruction step, a credential store, an SMTP sender, a retry and logging path, and a monitor watching the whole thing for the day it stops.
Same outcome for the client. One path is a checkbox in a settings screen. The other is a background service we'd be patching a year from now. The diagram below draws both — the custom IMAP-poll and SMTP layer struck through, the native two-way sync kept.
What broke / what surprised us
Nothing broke, because we shipped nothing to break. That is the quiet win of this kind of decision: the surprises live in the code you never wrote.
The surprise was how complete the native sync already was. Going in, the assumption baked into the ticket was that GHL couldn't do full two-way email on its own — that a mailbox connection would be one-directional, or drop threading, or need a custom bridge to associate mail with contacts. Testing said otherwise on every count. Inbound, outbound, contact association, threading, and authentication were all handled.
The second surprise was how easy it is to scope a build around a gap that isn't there. The ticket was well written. It described a coherent system. It just described a system the platform already runs. The failure mode wasn't bad engineering — it was writing the spec before reading the platform.
Results
The result is the code that doesn't exist and the maintenance that never starts.
The client gets exactly what they asked for — two-way email inside the CRM, mail on the right contact, threads intact, sending and receiving in one place — through a native feature they already had access to. We got them there by turning it on, not by standing up a service.
And the cost avoided is concrete. No IMAP poller to keep from falling behind. No dedup logic to debug at the edges. No credential store to secure. No retry-and-logging path to babysit. No sync monitor to watch. No background jobs, no on-call surface, no line of the custom sync to maintain. All of that was on the ticket. None of it got built.
We won't attach a manufactured time or money figure to this. The honest result is architectural: the client runs on a feature the vendor maintains, and we own zero of it.
Takeaways
- Read the platform before you spec the build. The most expensive integrations are the ones that duplicate a feature already shipped. Spend an hour testing what the platform does before you spend a sprint rebuilding it.
- "Buy" often means "it's already included." Build-vs-buy isn't only about third-party tools. Sometimes the buy is a native feature the client already pays for and hasn't turned on.
- Count the maintenance, not just the build. A sync isn't done when it works once. It's a poller, dedup, OAuth, retries, and monitoring — forever. Price the forever.
- Deleting a ticket is a deliverable. The recommendation not to build was the work. A clear "you don't need this, here's why" is worth more than a feature nobody should own.
- Let the vendor carry the boring parts. Authentication, provider quirks, and threading are solved problems on a mature platform. Owning them yourself buys risk, not value.
Ready to Implement These Strategies?
Let's discuss how to apply these insights to your specific business challenges.
Schedule Consultation