Going Hybrid: Bolting Mailcow Onto Microsoft 365 (And the One Line That Made the Whole Exchange Detour Pointless)

The sequel nobody asked for: I needed my apps to send mail, and rather than use Microsoft's perfectly good printer-relay method I funnelled the whole thing through my self-hosted mailcow as a split-domain hybrid - then ran Exchange's AD prep chasing a targetAddress fix that did nothing, before a single line of PowerShell solved it all.

Last time, I stood up a two-node mailcow cluster and signed off with a vague threat about spinning Exchange up “for giggles.” Reader, some giggles have arrived, and they cost me a weekend, a domain controller’s dignity, and a non-trivial amount of self-respect. To set expectations early, though: I never actually stood Exchange up fully, and I never ran all three systems against each other. But the option to really hurt myself is sitting right there, fully loaded - between Exchange Online, mailcow, and an on-prem Exchange I got close enough to taste, I’m one bad weekend away from three mail systems each holding a different opinion about who owns my domain. I didn’t pull that trigger. I’m just noting the gun exists, and that it’s pointed roughly at my own foot.

This is the post where I take my lovingly over-engineered, self-hosted mailcow server and bolt it onto Microsoft 365 in a hybrid configuration - so the same domain can send and receive across both systems at once: Microsoft holding the real mailboxes, mailcow handling app mail and overflow on the same domain, and mail flowing cleanly between the two. It is, in principle, a tidy idea. In practice it is two mail systems that each believe they are in charge, introduced to one another and told to share a domain like civilised adults. They did not, at first, behave like civilised adults.

As before: this is not a guide. It’s a confession with code blocks. If you want the real documentation, Microsoft’s hybrid and connector docs and the mailcow relay docs are written by people who haven’t done what I’m about to describe. Learn from their competence, then come back and laugh at me.

Why on earth would you do this?

Here’s the actual driver, and it starts dumber than “I wanted a hybrid”: I need apps to be able to send mail. Self-hosted apps, one-off projects, the various bits and pieces that fire off a notification or a password-reset from an address on one of my domains. That’s the whole requirement - outbound mail, from apps, on my domains. Meanwhile thefathacker.tech already has its MX pointed at Microsoft 365 - the @ MX thefathacker-tech.mail.protection.outlook.com from the DNS section of the last post was foreshadowing you weren’t meant to notice yet - and Microsoft holds my “real” mailboxes. I’m not de-clouding those overnight; I said as much last time and meant it.

And before anyone reads this as Microsoft-bashing: it isn’t. Credit where it’s due - Microsoft’s infrastructure, spam filtering, and malware protection are dramatically better than anything I can stand up in a homelab, and I have no illusions otherwise. That’s a real benefit of this arrangement, not an afterthought: mail crosses Microsoft in both directions - inbound through the MX, outbound through their smart host - so I get their reputation and their filtering on both legs, which comfortably beats anything I’d run myself.

The catch is mostly licensing rather than capability. This is my lab, on a commercial E5 licence - and if you don’t already know what an E5 seat costs per month, here’s my one genuine piece of advice in this whole post: do not look it up. Some numbers can’t be un-seen, and that one will follow you home and sit at the foot of your bed. I pay the commercial rate; I just don’t run a business on it. It’s personal and homelab - Microsoft’s already in the picture for a dozen unrelated things, so that bill is sunk either way.

What I cannot justify is a separate licensed mailbox for every app that needs to fire off the occasional notification; that math falls apart the moment you have more than a couple. mailcow gives me effectively unlimited mailboxes for those apps at the cost of RAM I’ve already spent, while the paid E5 seats stay reserved for the humans who actually need Outlook, calendars, and Teams.

And if Microsoft keeps nudging that price up - history strongly suggests they will - there’s a ceiling I can’t clear, and the day I hit it I’m gone. More on how short that exit actually is later.

There is a documented, Microsoft-blessed way to let apps send mail through 365: the multifunction device or application flow - the “multifunction printer” method. You stand up an SMTP relay connector, point your app at it, and it relays mail out for you, external domains included. Printers, scanners, and line-of-business apps lean on it constantly.

So why am I not just using it? Because for my situation it collects a pile of limitations that range from annoying to outright disqualifying:

  • No real authentication. The relay connector trusts the sending IP (optionally a certificate), not a username and password. There’s no per-app SMTP AUTH credential to issue and rotate - you’re trusting an address, not an identity.
  • CGNAT kills it outright. Because that trust hangs on a static, unique public IP, the moment a sender sits behind CGNAT - no stable public address to pin the connector to - the whole method collapses. A fair few of the places I’d want to send from are exactly that.
  • Sender scope is internal-only. It expects to relay as your own accepted domains with your IP listed in SPF; it is not a general-purpose “send as whatever I like” service, and the SPF assumptions come with it.
  • TLS is limited next to a proper authenticated submission service.
  • Connectors are upkeep. Every IP-and-domain combination is another connector to provision, document, and babysit - ongoing maintenance I’d rather not own.
  • The apps can’t do modern auth anyway. Even ignoring the relay, authenticating straight to Exchange Online is out - Microsoft has deprecated Basic Authentication in favour of modern auth (OAuth2) or the Graph API, and my apps speak neither, only plain SMTP with a username and a password. mailcow still accepts that humble login; modern Exchange Online increasingly does not.

So this whole post is my ridiculous way of routing around all of that: instead of a fleet of fragile IP-pinned 365 connectors, apps authenticate to the mailcow box I already built - real credentials, real TLS, works from anywhere including behind CGNAT - and mailcow handles the relay into and out of Microsoft. The MFP method is a screwdriver that only works if you’re standing in precisely the right spot. I have, instead, machined a screwdriver that works from across the room. Same screw, considerably more of my weekend.

And yes, before the inevitable comment: I know the “sensible” answer here is a dedicated email service - SendGrid, Mailgun, Postmark, Amazon SES, take your pick. They exist precisely for “my app needs to send mail,” they’re genuinely good at deliverability, and for plenty of people they’re the right call. I just don’t like them. The free and cheap tiers come wrapped in sending caps - so many a day, so many a month - and the instant you care about a cap you’ve signed yourself up to monitor it: watch the quota, get paged as you approach it, juggle which app sends through which key so none of them tips over the edge. That’s a whole category of operational babysitting I flatly do not want. I would rather spend RAM I already own than spend my attention watching someone else’s meter. Your priorities may differ, and that’s completely fine - this is a preference, not a verdict.

And underneath that grumble is a plainer truth, so I’ll just say it: I like to own the things I depend on. Where it’s realistically possible, I would rather run my own service and keep my own data on my own hardware than rent both from someone else. It’s a personal philosophy more than a technical argument - the same instinct that had me self-hosting mail in the first place - and self-hosting carries its own very real costs that I’m not going to pretend away. But “I control it, and the data lives on my iron” is worth genuine effort to me. (Yes, I’m aware of the irony of saying that one breath after admitting Microsoft holds my actual mailboxes. De-clouding is a journey, not a light switch, and this whole hybrid is me taking one more step down that road.)

The mechanism that makes it hang together is a split domain: Microsoft 365 stays the authoritative front door for thefathacker.tech, mailcow handles the app-and-overflow mail on the same domain, and the two pass mail between them as needed. The phrase Microsoft uses for “two systems share one domain and relay between them” is internal relay, and that phrase is going to do a tremendous amount of load-bearing work later. Hold onto it.

The shape of the thing

Before the gory bits, the architecture in one breath:

  • Microsoft 365 / Exchange Online owns the MX. All inbound mail for the domain hits Microsoft first.
  • If the recipient is a real mailbox in Exchange Online, it’s delivered there. Done.
  • If the recipient isn’t in Exchange Online, Microsoft doesn’t reject it - instead it relays the message onward to mailcow over a connector. This is the bit that requires the magic words.
  • mailcow hosts those overflow mailboxes locally, and for anything it doesn’t host, it relays back toward Microsoft.
  • Outbound mail from the mailcow senders - the apps and any local mailboxes alike - egresses through Exchange Online via a sender-dependent transport, so everything leaves the building under Microsoft’s well-warmed IP reputation instead of my colo range. This is the whole reason the apps point at mailcow in the first place.

Two connectors, one relay domain, one accepted-domain setting, and a sender-dependent transport. That’s the whole machine. It took me an embarrassing detour through Exchange’s AD prep to discover the machine needed none of it.

Design note: only one domain takes the scenic route

Worth being explicit about scope, because this is a deliberate choice and not an accident: the hybrid above applies to exactly one domain, thefathacker.tech. That’s the domain whose mailboxes I actually keep in Microsoft 365, so it’s the only one that needs Exchange Online in the path at all.

Every other domain I host - fathaxz.wtf and friends - skips Microsoft entirely and talks straight to mailcow. Their MX points at my server, mail arrives directly, and there is no connector, no internal relay, and no Exchange Online anywhere in the loop. They were never in the cloud and don’t need to be.

flowchart LR
    subgraph hybrid ["thefathacker.tech (hybrid - this whole post)"]
        direction LR
        net1@{ shape: cloud, label: "Internet" }
        exo@{ shape: cyl, label: "Exchange Online" }
        mc1@{ shape: cyl, label: "Mailcow" }
        box1@{ icon: "mynaui:inbox-down", label: "Real mailboxes" }
        box2@{ icon: "mynaui:inbox-down", label: "App + relay mailboxes" }
        net1 -->|fa:fa-envelope Inbound| exo -->|fa:fa-envelope Outbound| net1
        exo -->|fa:fa-envelope Inbound| mc1 -->|fa:fa-envelope Outbound| exo
        exo --- box1
        mc1 --- box2
    end
    subgraph direct ["fathaxz.wtf (every other domain I host)"]
        direction LR
        net2@{ shape: cloud, label: "Internet" }
        mc2@{ shape: cyl, label: "Mailcow" }
        box3@{ icon: "mynaui:inbox-down", label: "Mailboxes" }
        net2 -->|fa:fa-envelope Inbound| mc2 -->|fa:fa-envelope Outbound| net2
        mc2 --- box3
    end

So every bit of connector-and-accepted-domain pain that follows is the price of one domain’s split personality. The domains that live entirely on mailcow stayed refreshingly simple, exactly as the last post left them.

The mailcow side (the easy half)

I’ll start here because mailcow, bless it, made this part painless - which lulled me into a false sense of security I would pay for within the hour.

A relay domain, not a hosted one

In the mailcow admin console I added thefathacker.tech, but not as a normal hosted domain. It goes in as a relay domain with these options ticked:

  • Relay this domain - yes, mailcow, you are now in the business of this domain.
  • Relay all recipients - accept mail for any recipient under it.
  • Relay non-existing mailboxes only - and this is the important one: existing mailboxes (the ones I actually create in mailcow) get delivered locally, while everything else is relayed onward. This is exactly the split-domain behaviour I want, from mailcow’s side of the fence.

I also set the DKIM selector for this domain to dkim-mailcow rather than the default, purely so it doesn’t collide with the selector1/selector2 CNAMEs Microsoft already manages for the same domain. Two systems signing for one domain means two distinct DKIM selectors, full stop - step on each other’s selector and you’ll spend an evening wondering why half your mail fails DKIM.

Sender-dependent transport: leave through the front you came in

Here’s the genuinely clever-feeling part. Mail from the mailcow-hosted thefathacker.tech mailboxes shouldn’t just fling itself at the internet from my colo IPs - it should leave the way the rest of the domain’s mail leaves: through Microsoft. mailcow’s sender-dependent transports do exactly this. One entry:

ID Host For senders from
1 thefathacker-tech.mail.protection.outlook.com thefathacker.tech

Translation: “when a message is sent by a thefathacker.tech address, route it to Microsoft’s smart host instead of delivering it yourself.” Outbound mail from my self-hosted mailboxes now rides out under Microsoft’s IP reputation, which is about as warmed-up as mail reputation gets. My colo range stays out of the deliverability blast radius entirely.

DNS: hands off autodiscover

One small but critical DNS decision: when adding the domain in mailcow, I chose “Do not autoconfigure DNS.” mailcow wants to manage autodiscover/autoconfig records so its clients self-configure - but on this domain, Microsoft 365 owns autodiscover. Let mailcow plant its own autodiscover records here and Outlook clients will get pointed at the wrong server and configure themselves into a corner. So mailcow keeps its hands in its pockets on DNS for thefathacker.tech, and Microsoft’s autodiscover stays authoritative. The mailcow-hosted mailboxes get configured manually; it’s a handful of addresses, not a fleet.

That’s the mailcow half done. It was tidy, it was documented, it worked. Then I turned to the Microsoft side, and the universe restored balance.

The Microsoft side (the half that broke me)

A 550 and a feeling of dread

I wired up the first connector, sent a test, and Exchange Online greeted me with this:

550 5.1.10 RESOLVER.ADR.RecipientNotFound;
Recipient [email protected] not found by SMTP address lookup

For the uninitiated, RESOLVER.ADR.RecipientNotFound is Exchange’s way of saying: “I am authoritative for this domain, I looked in my own directory, I did not find this person, and therefore this person does not exist anywhere in the universe.” Exchange was treating thefathacker.tech as a domain it owned completely - so any recipient not in Exchange Online was, by its lights, fictional. It never even considered relaying to mailcow.

Here’s the part I’ll own up front, though: none of this was a surprise. The behaviour is documented in mailcow’s own docs (which, I’ll gently note, could stand to explain it a good deal better), and I walked straight into the 550 on purpose. I was deliberately trying to keep Exchange Online authoritative for the domain, because an authoritative EXO validates that a recipient actually exists before it ever hands a message to the connector. That validation is a genuinely attractive property: it’s a clean guard against mail loops - the classic hybrid failure where two systems each insist the other is responsible for some nonexistent recipient and ping the poor message back and forth until something mercifully gives up. Keep EXO authoritative, the theory went, and I’d get loop protection for free.

The flaw in that theory is the entire problem: the same recipient validation that would prevent loops is precisely what rejects my legitimate mailcow-hosted recipients with a 550. You cannot have EXO both vet every recipient against its own directory and forward the ones it’s never heard of - authority and relay are mutually exclusive here. So that plan - keep authority, get loop protection thrown in - was a flat failure. Whelp.

Which lands on the heart of the whole thing: the domain is set as an authoritative accepted domain, and it needs to be an internal relay accepted domain instead. Authoritative means “every recipient lives here or nowhere.” Internal relay means “I host some of these; anything I don’t recognise, hand off to my partner system.” That one setting is the difference between a working hybrid and a wall of 550s - and the price of admission is surrendering the tidy loop-prevention guarantee I’d been clinging to. Loops become something I have to respect rather than something the design quietly rules out for me.

I knew the words. I did not, at this point, know they were the entire fix. So instead of typing the obvious, I went and did something deeply silly.

The silly thing: I ran Exchange’s AD prep (and stopped there)

Classic Microsoft hybrid lore says that to manage recipients for a shared domain you reach for on-premises Exchange - the historical “you need an Exchange server in the mix even for a hybrid” guidance. The specific thing I was chasing was the ability to stamp targetAddress (and friends) onto Active Directory objects, so the directory would carry an external delivery address for the mailcow-bound recipients.

To get those attributes, you don’t need a running Exchange server - but you do need Exchange to have prepared Active Directory, which means extending the AD schema. So I grabbed the Exchange Server Subscription Edition ISO purely to run the prep:

  • Exchange Server Subscription Edition RTM (KB5047155), version 15.02.2562.017
  • ExchangeServerSE-x64.iso, a brisk 6.0 GB

To be crystal clear, because I’d talked myself into thinking I needed more: I never actually installed Exchange. No mailbox role, no server, no services - just /PrepareSchema and /PrepareAD. The 6 GB ISO was a glorified ldifde delivery mechanism. And even that much made me fight Active Directory first.

Patch the domain controllers, you absolute walnut

My own notes contain a message from past-me to present-me, transcribed here with the original punctuation intact because it deserves to be immortalised:

PATCH THE DAM DCs because otherwise the Exchange SE Install Crashes idiot. Needs at least November 2025 Cumulative Update for Server 2025 (Assuming you are and using Server 2025 Domain Controllers).

The Exchange SE installer extends the Active Directory schema, and on under-patched Server 2025 domain controllers, the schema extension doesn’t politely decline - it detonates. Here’s the carnage from the first /PrepareSchema run:

    Extending Active Directory schema                          FAILED

There was an error while running 'ldifde.exe' to import the schema file
'...\PostWindows2003_schema60.ldf'. The error code is: 8245.
Entry DN: CN=ms-Org-Leaders-BL,CN=Schema,CN=Configuration,DC=thefathacker,DC=net
Add error on entry starting on line 282: Unwilling To Perform
The server side error is: 0x20cc Schema update failed in recalculating validation cache.
000020CC: SvcErr: DSID-03260334, problem 5003 (WILL_NOT_PERFORM), data -536805373

WILL_NOT_PERFORM is Active Directory’s equivalent of folding its arms: the schema validation cache on an under-patched Server 2025 DC simply refuses the new Exchange attributes. The fix was exactly what past-me screamed - patch the domain controllers and re-run - and the specific patch is the November 2025 cumulative update for Windows Server 2025, KB5068861 (OS build 26100.7171). My environment made the remedy refreshingly unambiguous: every DC is Server 2025, sitting at Windows Server 2016 forest functional level - a single-version forest, no older DCs to placate. So the entire fix was: install the updates I’d been putting off, reboot, and run /PrepareSchema again. Second time, it completed:

    Extending Active Directory schema                          COMPLETED
The Exchange Server setup operation completed successfully.

Then /PrepareAD to prep the organisation:

.\Setup.exe /PrepareAD /OrganizationName:"thefathacker" /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF

    Organization Preparation                                   COMPLETED
The Exchange Server setup operation completed successfully.

One honest footnote, because what KB5068861 actually documents fixing is subtler than the brick wall I hit. On Server 2025 schema masters, /PrepareSchema could silently succeed while quietly writing duplicate multi-valued schema attributes, and the damage only surfaced later as AD replication failures - Event ID 8418 and NTDS 1203 - usually in a mixed-version forest where older DCs reject the mangled schema. The November update prevents those duplicates being created; it explicitly does not repair conflicts already present, and Microsoft’s documented mitigation even includes running the prep from a non-2025 schema master (a 2022 DC) to sidestep it entirely. My failure was the loud version - a flat FAILED at “recalculating validation cache,” not a quiet success that poisons replication days later - so it’s plausibly a related-but-distinct 2025 schema-engine bug rather than that exact defect. I’m not going to pretend I reverse-engineered precisely which gremlin bit me. What I can tell you cleanly is that on an all-2025 forest the same November CU cleared it, and /PrepareSchema then completed honestly.

So now I had a freshly schema-extended Active Directory and the ability to stamp targetAddress onto users - all in service of an approach that was about to not work at all.

The proxyAddresses rabbit hole

This was the entire point of the schema prep: managing recipients as mail-enabled AD objects, so the directory would “know” about the mailcow mailboxes and route to them. It means stamping AD users with the right attributes - targetAddress, proxyAddresses, mail - so they’re treated as having an external delivery address.

Set-ADUser -Identity test -Add @{targetAddress="SMTP:[email protected]"}
Set-ADUser -Identity test -Add @{proxyAddresses="SMTP:[email protected]"}
Set-ADUser -Identity test -Replace @{mail="[email protected]"}

Two genuinely useful things fell out of this detour, so it wasn’t entirely wasted:

  • Capitalisation in proxyAddresses is meaningful. A capital SMTP: marks the primary address; lowercase smtp: marks a secondary alias. Get this backwards and the user’s primary identity shifts under you. The protocol is case-insensitive; this attribute is petty about it.
  • My notes also contain the plaintive question “do I need targetAddress or only proxyAddresses???” and the answer, after much flailing, is: for the approach I actually ended up using, neither. Which brings us to the part where I admit the whole on-prem adventure was unnecessary.

The one line that did everything

After the ISO, the schema extension, the DC patching, the WILL_NOT_PERFORM, the proxyAddresses incantations - here is what actually fixed the RecipientNotFound 550 and made the entire hybrid work. It is one line. You run it in Exchange Online PowerShell. You do not need an on-prem Exchange Server to run it. You did not need to install 6 GB of anything.

Connect-ExchangeOnline
Set-AcceptedDomain -Identity thefathacker.tech -DomainType InternalRelay

That’s it. That’s the post. Flipping the domain from Authoritative to InternalRelay tells Exchange Online: “you do not own every recipient under this domain - if you can’t find someone locally, relay the message onward rather than bouncing it.” The moment that setting flipped, the 550s stopped and mail for unknown recipients started flowing to the connector I’d pointed at mailcow.

I want to be very clear about the lesson here, because it’s the whole reason this post exists: the entire AD-prep-and-targetAddress detour was solving a problem I didn’t have. Mail-enabling AD objects is a real, valid technique for big, traditional hybrid deployments. But for “Microsoft 365 holds the MX and I want some mail to live elsewhere on the same domain,” the requirement collapses to a single accepted-domain setting and a pair of connectors. I extended my Active Directory schema to hang one picture frame.

The connectors

With the domain set to internal relay, two connectors finish the loop. Both live in the Exchange admin connectors page.

From Microsoft 365 → mailcow

This is the connector that catches the relayed overflow and delivers it to my server.

  • From: Office 365
  • To: Your organization’s email server
  • Name: => mail.thefathacker.tech (thefathacker.tech)
  • Use of connector: only when mail is sent to these domains - thefathacker.tech and *.thefathacker.tech
  • Routing destination: mail.thefathacker.tech
  • Security: always use TLS, require the certificate be issued by a trusted CA, and require the subject/SAN to match mail.thefathacker.tech. No self-signed certificates - which is exactly why mailcow gets real Let’s Encrypt certs of its own, a point I laboured last time. Past-me did present-me a favour for once.

From mailcow → Microsoft 365

The return path, so mailcow can hand mail back to Microsoft for delivery and outbound.

  • From: Your organization’s email server
  • To: Office 365 (the only option, naturally)
  • Name: <= mail.thefathacker.tech (thefathacker.tech)
  • Authenticating sent email: by certificate, SAN mail.thefathacker.tech

Note that both connectors authenticate on the certificate SAN, not on IP allow-lists. That’s deliberate: IPs change, certificates are identity. It also means the whole trust relationship rides on the same Let’s Encrypt cert mailcow already renews over DNS-01, so there’s nothing extra to rotate.

Where things stand

It works. Mail to a Microsoft-hosted mailbox lands in Microsoft. Mail to a mailcow-hosted mailbox on the same domain relays cleanly through to my server. Outbound from the self-hosted side leaves through Microsoft’s smart host with Microsoft’s reputation. One domain, two backends, and - crucially - zero on-premises Exchange Servers in the final design, despite the detour that very nearly talked me into one.

A note to future me: the off-ramp is already built

Here’s the part that quietly pleases me, tying back to the de-clouding instinct from earlier. Because of how this hybrid is wired, the day I decide to pull a mailbox - or the whole domain - off Microsoft entirely, the operational lift is almost embarrassingly small. The path off the cloud is three moves:

  1. Create the mailbox in mailcow. The domain is already a relay domain there, so a real local mailbox is just a few clicks; existing mailboxes already get delivered locally.
  2. Update DNS. Point the MX (and autodiscover/autoconfig) at mailcow instead of Microsoft, so the world starts delivering straight to my server.
  3. Turn off the smart host. Drop the sender-dependent transport that currently routes outbound through Outlook, and mailcow sends for itself.

Three steps and the domain lives entirely on my own iron. Tada - off cloud.

The one genuine hurdle isn’t the cutover, it’s the history: years of existing mail sitting in Exchange Online has to be archived and migrated across into mailcow, and that’s the heavy, fiddly bit. But it’s a solved problem - imapsync and friends do exactly this - so it’s a chore, not a blocker. The point is that the architecture leaves the exit door unlocked rather than welding it shut.

There’s a deeper reason I keep that door oiled, too. The thing genuinely pinning me to Microsoft isn’t mail - mailcow does mail fine - it’s the rich Outlook-and-mobile experience: calendars, contacts, push, the MAPI and ActiveSync glue that makes a phone and a desktop just work together without thinking about it. That used to be the one thing only Exchange could really deliver, and it increasingly isn’t: open-source ActiveSync and MAPI implementations keep maturing, and the day they’re solid enough, the last functional reason I’m renting a mailbox from Redmond quietly evaporates. That’s the milestone I’m actually waiting on - not the three-step cutover, which is trivial, but the feature parity that makes the cutover worth doing.

So to be clear about where I land: it’s “easy,” the off-ramp is built, and it is emphatically not happening any time soon. Knowing I can leave is most of the value; actually leaving is a problem for a future me with more spare weekends than this one.

Things not yet done

The TODO list is a series tradition:

  • Live with the AD schema extension - I never installed Exchange, but /PrepareSchema and /PrepareAD permanently altered Active Directory. The changes are benign, but they happened, and future-me deserves a note explaining why there’s Exchange schema in a forest with no Exchange in it.
  • Outbound firewall rules for the connector path - same gap I flagged last time, now with more surface area.
  • Document the per-mailbox split - which addresses live where is currently in my head, which is not a backup strategy.
  • Revisit DKIM alignment across both signers - two systems signing one domain deserves a careful re-check that both paths pass DMARC, not just the one I tested.
  • Test failover with the hybrid in place - the cold spare is still untested, and now there’s a connector relationship to validate too.

A verdict on the hybrid

Honestly? It works, and the mechanics are clean where it counts. Connectors plus a sender-dependent transport - routing all self-hosted outbound through Microsoft’s smart host - is the single best deliverability decision in the whole build, and mailcow’s relay-domain and sender-dependent-transport features slot into the pattern almost suspiciously well.

What I’ll be honest about, though, is that I don’t actually like the setting that makes the whole thing go: internal-relay mode. Flipping thefathacker.tech off authoritative is what unblocked everything, and it’s also the part I’m least comfortable with. An authoritative Exchange Online checks recipients against its own directory and refuses to guess; internal relay tells it to stop checking and forward anything it doesn’t recognise straight to mailcow. That’s the loop-prevention guarantee I grumbled about giving up, now formalised as the design instead of a regret - I’ve effectively granted two mail systems standing permission to lob unknown recipients at one another and appointed SMTP’s hop counter as the only adult in the room. It is the correct setting for this architecture, and I still don’t trust it the way I trust a domain that validates its own recipients. If the design let me keep authoritative validation and relay the overflow, I’d take that trade in a heartbeat. There’s probably even a version where I could: a real, fully-installed on-prem Exchange would give me an actual directory to validate against and might just square the circle. But that road dead-ends at three live mail systems quietly disagreeing about who owns my domain - the loaded gun from the very top of this post - and so far, mercifully, I don’t hate myself quite that much. So internal relay it is - eyes open, slightly twitching.

If you take one thing from this post, take the cheap one: before you reach for the heavy thing, check whether the light thing already does the job. Mine was one line of PowerShell, sitting there free and smug the whole time. And understand that you can absolutely over-build a problem into something far more complicated than it ever was - sometimes the right path really is just the straight line between two points, no matter how badly I want to add a third.

Read the error message. It was telling me the answer in its own grumpy dialect from the very first 550. I just wasn’t fluent yet.


Disclaimer, as ever: this post was written with the help of Claude Opus, and as ever I’ve read and approved every word - including the parts where I’m the punchline. The technical decisions, the unnecessary trip through Exchange’s AD prep, and the all-caps note to patch the domain controllers are regrettably, verifiably, entirely my own. The cold spare is still untested. Keep a fire extinguisher near the rack. Or don’t, i’m not your mum.