All posts
Technical

Setting up email for cold outreach that actually lands

SPF, DKIM and DMARC are the part everyone writes about, and they took an afternoon. The thing that actually bounced a third of one campaign was the list. Both, from a one-person domain that has sent a few hundred cold emails.

Over the last six weeks I have sent a few hundred cold emails from one domain: first to local businesses around Devon, then to UK ecommerce merchants. Along the way the domain got its authentication set up properly, one campaign bounced 19 of its first 62 sends, and Gmail quietly rewrote my signature into something that looked like a mass mailer. This is what I would set up on day one if I were doing it again, in the order it matters.

Part one: the DNS, which is the easy part

There are three records. If any of them is missing, Gmail and Outlook treat the mail as unauthenticated, and since early 2024 Google and Yahoo both require the first two from anyone sending in volume. At cold-outreach volumes you are not a bulk sender, but you are exactly the kind of sender their filters were built to be suspicious of, so do all three anyway.

SPF says which servers may send for your domain. If your mail is on Google Workspace, it is one TXT record on the root of the domain:

v=spf1 include:_spf.google.com ~all

Add an include: for anything else that sends as you, such as a transactional relay, and keep it to one record. Two SPF records on the same name is a fail, not a merge.

DKIM signs each message so the receiver can prove it was not altered in transit, and it is the one people miss, because on Google Workspace it is not on by default. Until you set it up, Google signs your mail with a generic key on its own domain, which passes DKIM in the narrow sense and fails DMARC alignment in the sense that matters. In the admin console it is Apps, Google Workspace, Gmail, Authenticate email: generate the key for your domain, publish the TXT record it gives you at google._domainkey, wait for DNS, then come back and press Start authentication. That last click is the step that gets forgotten. The record can be published for weeks and nothing is signed until you press it.

DMARC tells receivers what to do when SPF or DKIM fail, and asks them to send you reports. Start at p=none, which changes nothing about delivery and just turns on the reports:

_dmarc.yourdomain.com  TXT  "v=DMARC1; p=none; rua=mailto:[email protected]"

The reports are XML attachments from Microsoft, Yahoo, Google and anyone else who received mail claiming to be you, and they arrive daily. Mine reached 31 in the inbox before I built a rule to file them. Read a few in the first fortnight: if every source in them is one you recognise, move to p=quarantine. If there is a source you do not recognise, that is either something you forgot you set up or somebody spoofing you, and either way you want to know before you tighten the policy.

That is the whole of part one. It is an afternoon, most of it waiting for DNS, and it is table stakes rather than an advantage.

Part two: do not let your robots share a reputation with you

My n8n workflows send email too: quotes, invoices, the auto-reply to the website’s contact form. For a while all of that went out through the same Workspace account as the outreach, on a connected Gmail credential.

That is fine at low volume, and it is a mistake waiting to happen at any other volume. A sending reputation belongs to a domain and an IP, and everything that shares them rises and falls together. The day an automation misfires and sends two hundred invoices in a loop, the hand-written email you sent to a prospect that afternoon lands in spam with them.

The fix is a transactional relay for the automated mail, on its own subdomain, with its own SPF include and its own DKIM key. The system email goes out as billing@ or notify@ on the subdomain; the human email stays on the root domain and keeps its own record. Google’s own sending limits (2,000 messages a day per Workspace user) are rarely the constraint. Reputation is.

Part three: the list, which is where the bounces actually came from

Here is the number that matters. Across the first outreach motion, 40 first-contact emails, two bounced: one to a domain that had lapsed and was forwarding through a registrar that rejected the mail, one to an address copied from a local directory that was simply dead.

In the second motion, in one batch of 26, 14 bounced. Every single one was “mailbox does not exist”: 550 5.1.1 from Google, the 550 5.4.1 Access denied that Microsoft 365 uses to mean no such user, one 554 user does not exist. Not a spam block, not a reputation problem, not anything in part one. The addresses were wrong.

They were wrong because they had been guessed. The list had been built with role-based patterns, info@ and hello@ on the merchant’s web domain, and never checked against the merchant’s own site. When I went back and read every one of those sites, contact page, footer, FAQ, privacy policy, the causes were mundane:

  • The mail domain was not the web domain. A .co.uk shop with mail on .com, or a brand whose mail runs on the parent company’s domain.
  • The local part was different. hello@ where the site says info@, or the reverse, or something the guess could never have found, like buzz@ or hellohoney@.
  • The address on the site was a chat-widget vendor’s, not the merchant’s. It looked like an inbox and it was a form.

Seventeen of the nineteen bounced merchants published a real address. It was there the whole time; nobody had looked.

So the rule now, and it is the only rule in this post that saved money: no address goes into a draft until it has been seen on the merchant’s own site or on the owner’s LinkedIn. A guess costs a bounce, and a bounce is not free. Receivers count them, and a domain that bounces a third of what it sends starts to look like a domain that sends to lists it did not build.

Part four: the small things that read as mass mail

None of these will show up in a bounce log. They show up as silence.

Gmail rewrote my signature. The signature ended with a bare risdonautomation.com. Gmail auto-linked it, and the link it wrote was not the domain, it was google.com/url?q=...&source=gmail&ust=..., a tracking redirect, in the plain-text version of the message as well as the HTML. A hand-written 130-word email with a Google tracking wrapper at the bottom is exactly the tell a careful reader is looking for. I only caught it because I read a draft back through the API. The fix was to drop the URL: the sender address already has the domain in it.

No tracking pixel. An open-tracking image is the same tell, and it is also the reason you can never be sure whether the silence is disinterest or a spam folder. I chose not to know.

Plain text, under 110 words, no bullets, no exclamation marks, contractions. Not for deliverability exactly, but a filter trained on marketing mail is trained on marketing formatting, and the reader is too.

One follow-up, fourteen days later, as a reply in the same thread. A second touch performs at or above the first and carries no new list risk. A third does not, and it starts to look like a sequence.

Spread the sends. Nine in a day rather than twenty-four, from a domain that has never sent twenty-four cold emails in a day before. The limit you are respecting is not Google’s. It is the pattern.

The honest part

A clean bounce log proves that the receiving server accepted the message. It does not prove the message reached an inbox, and without a tracking pixel there is no way to know. The only evidence that any of this works is a reply, and replies are rare enough that it takes a hundred sends to learn anything.

So do the DNS in an afternoon, separate the robots from the humans, and then spend the time you saved reading contact pages. The third of those is the one that moved a number.