Back to Blog

Email Deliverability: SPF, DKIM, DMARC, and Getting Your Emails to the Inbox

Fix email deliverability with SPF, DKIM, and DMARC setup, IP warm-up, bounce handling, and list hygiene. Includes DNS records, code examples, and sender reputat

Viprasol Tech Team
April 10, 2026
11 min read

Email Deliverability: SPF, DKIM, DMARC, and Getting Your Emails to the Inbox

Email deliverability is binary from a user's perspective: either they got it or they didn't. But the systems that determine inbox vs. spam are complex, and most teams don't think about them until they're staring at a 30% delivery rate on a critical campaign.

This guide covers the full deliverability stack โ€” authentication records, sender reputation, bounce handling, and list hygiene โ€” with the specific configurations that move emails from spam to inbox.


Why Emails Go to Spam

Gmail, Outlook, and other providers use a combination of signals to score every incoming email:

Signal CategoryExamplesWeight
AuthenticationSPF pass/fail, DKIM signature, DMARC policyVery High
Sender reputationIP reputation, domain age, complaint historyVery High
EngagementOpen rate, click rate, reply rate, not-spam reportsHigh
ContentSpam trigger words, HTML quality, text-to-image ratioMedium
List qualityBounce rate, unsubscribe rate, spam trapsMedium
Volume patternsSudden volume spikes, sending from new IPsMedium

Authentication failures (missing SPF, no DKIM) are the fastest path to the spam folder and the easiest to fix.


Step 1: SPF (Sender Policy Framework)

SPF tells receiving mail servers which IP addresses are allowed to send email for your domain.

DNS TXT record for yourdomain.com:

; Allow Sendgrid, AWS SES, and your own mail server to send on behalf of your domain
v=spf1 include:sendgrid.net include:amazonses.com ip4:203.0.113.10 -all

Components:

  • v=spf1 โ€” SPF version
  • include:sendgrid.net โ€” authorize SendGrid's IP ranges
  • include:amazonses.com โ€” authorize AWS SES IP ranges
  • ip4:203.0.113.10 โ€” authorize a specific IP (your own mail server)
  • -all โ€” reject all other senders (hard fail)
    • Use ~all (soft fail) initially while testing โ€” less likely to accidentally block legitimate email

SPF limits: Maximum 10 DNS lookups per SPF record. Many include: statements push you over this. Use tools like dmarcian SPF Surveyor to check your lookup count. If you exceed 10, use an SPF flattening service.

Verify with:

dig TXT yourdomain.com | grep spf
# or
nslookup -type=TXT yourdomain.com

๐Ÿš€ SaaS MVP in 8 Weeks โ€” Seriously

We have launched 50+ SaaS platforms. Multi-tenant architecture, Stripe billing, auth, role-based access, and cloud deployment โ€” all handled by one senior team.

  • Week 1โ€“2: Architecture design + wireframes
  • Week 3โ€“6: Core features built + tested
  • Week 7โ€“8: Launch-ready on AWS/Vercel with CI/CD
  • Post-launch: Maintenance plans from month 3

Step 2: DKIM (DomainKeys Identified Mail)

DKIM adds a cryptographic signature to every outgoing email. The receiving server verifies the signature against a public key published in DNS, proving the email wasn't modified in transit and actually came from your domain.

How it works:

  1. Your email provider generates an RSA key pair
  2. The private key signs each email's header + body hash
  3. The public key is published in DNS
  4. Receiving server fetches the DNS record and verifies the signature

DNS TXT record (provided by your email provider):

; selector._domainkey.yourdomain.com
; "selector" is provided by your ESP (e.g., "s1" for SendGrid, "amazonses" for SES)

s1._domainkey.yourdomain.com.  TXT  "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC..."

Each email provider generates the key pair and gives you the DNS record to add. You never handle the private key directly.

DKIM for multiple providers (SendGrid for marketing, SES for transactional):

; SendGrid
s1._domainkey.yourdomain.com.  TXT  "v=DKIM1; k=rsa; p=..."

; AWS SES (each region gets its own selector)
amazonses._domainkey.yourdomain.com.  CNAME  amazonses._domainkey.us-east-1.amazonses.com.

Verify DKIM:

# Check if DKIM record is published
dig TXT s1._domainkey.yourdomain.com

# Test with a real email โ€” send to check-auth@verifier.port25.com
# You'll receive a detailed deliverability report

Step 3: DMARC (Domain-based Message Authentication)

DMARC tells receiving servers what to do with emails that fail SPF or DKIM โ€” and critically, it generates reports so you can monitor authentication failures.

Start in monitor mode (collect reports, take no action):

; _dmarc.yourdomain.com
_dmarc.yourdomain.com.  TXT  "v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com; ruf=mailto:dmarc-forensic@yourdomain.com; fo=1"

Move to quarantine after 30 days of clean reports:

_dmarc.yourdomain.com.  TXT  "v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc-reports@yourdomain.com"
  • pct=25 โ€” only apply the policy to 25% of failing emails initially

Full enforcement:

_dmarc.yourdomain.com.  TXT  "v=DMARC1; p=reject; rua=mailto:dmarc-reports@yourdomain.com; adkim=s; aspf=s"
  • p=reject โ€” reject emails that fail authentication
  • adkim=s โ€” strict DKIM alignment (from domain must match DKIM signing domain)
  • aspf=s โ€” strict SPF alignment

DMARC report parsing: The reports are XML and hard to read manually. Use a free DMARC report parser: DMARC Analyzer, MXToolbox, or Postmark's DMARC Digests.


๐Ÿ’ก The Difference Between a SaaS Demo and a SaaS Business

Anyone can build a demo. We build SaaS products that handle real load, real users, and real payments โ€” with architecture that does not need to be rewritten at 1,000 users.

  • Multi-tenant PostgreSQL with row-level security
  • Stripe subscriptions, usage billing, annual plans
  • SOC2-ready infrastructure from day one
  • We own zero equity โ€” you own everything

Step 4: Transactional Email Setup (AWS SES Example)

// lib/email.ts
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';

const ses = new SESClient({ region: 'us-east-1' });

interface SendEmailOptions {
  to: string | string[];
  subject: string;
  htmlBody: string;
  textBody: string;
  replyTo?: string;
  configurationSet?: string;  // For tracking opens/clicks/bounces
}

export async function sendEmail(opts: SendEmailOptions): Promise<string> {
  const toAddresses = Array.isArray(opts.to) ? opts.to : [opts.to];

  const command = new SendEmailCommand({
    Source: `Viprasol <noreply@mail.yourdomain.com>`,
    Destination: {
      ToAddresses: toAddresses,
    },
    Message: {
      Subject: { Data: opts.subject, Charset: 'UTF-8' },
      Body: {
        Html: { Data: opts.htmlBody, Charset: 'UTF-8' },
        Text: { Data: opts.textBody, Charset: 'UTF-8' },
      },
    },
    ReplyToAddresses: opts.replyTo ? [opts.replyTo] : undefined,
    ConfigurationSetName: opts.configurationSet ?? 'default',
  });

  const result = await ses.send(command);
  return result.MessageId!;
}

SES bounce and complaint handling (critical for deliverability):

// api/webhooks/ses.ts โ€” Handle SES notifications via SNS โ†’ SQS โ†’ your endpoint
interface SESNotification {
  notificationType: 'Bounce' | 'Complaint' | 'Delivery';
  bounce?: {
    bounceType: 'Permanent' | 'Transient' | 'Undetermined';
    bouncedRecipients: Array<{ emailAddress: string; diagnosticCode: string }>;
  };
  complaint?: {
    complainedRecipients: Array<{ emailAddress: string }>;
    complaintFeedbackType: 'abuse' | 'fraud' | 'other';
  };
}

export async function handleSESNotification(notification: SESNotification) {
  switch (notification.notificationType) {
    case 'Bounce':
      if (notification.bounce?.bounceType === 'Permanent') {
        // Hard bounce โ€” never send to this address again
        for (const recipient of notification.bounce.bouncedRecipients) {
          await db.emailSuppression.upsert({
            where: { email: recipient.emailAddress },
            create: {
              email: recipient.emailAddress,
              reason: 'hard_bounce',
              diagnosticCode: recipient.diagnosticCode,
            },
            update: { reason: 'hard_bounce', updatedAt: new Date() },
          });
        }
      }
      // Soft bounces: log but don't suppress (might be temporary)
      break;

    case 'Complaint':
      // User marked as spam โ€” suppress immediately
      for (const recipient of notification.complaint!.complainedRecipients) {
        await db.emailSuppression.upsert({
          where: { email: recipient.emailAddress },
          create: { email: recipient.emailAddress, reason: 'spam_complaint' },
          update: { reason: 'spam_complaint', updatedAt: new Date() },
        });
        // Unsubscribe from all marketing lists
        await db.user.updateMany({
          where: { email: recipient.emailAddress },
          data: { marketingEmailsOptIn: false },
        });
      }
      break;
  }
}

// Check suppression before every send
export async function canSendTo(email: string): Promise<boolean> {
  const suppressed = await db.emailSuppression.findUnique({
    where: { email },
  });
  return !suppressed;
}

Step 5: IP Warm-Up

Sending from a new IP address to a large list immediately will destroy your deliverability. ISPs see an unknown IP suddenly sending thousands of emails and assume it's spam.

Warm-up schedule:

DayDaily VolumeSegment
1โ€“350โ€“100Most engaged (recent openers)
4โ€“7500โ€“1,000Engaged last 30 days
8โ€“142,000โ€“5,000Engaged last 60 days
15โ€“2110,000โ€“20,000Engaged last 90 days
22โ€“3050,000+Full list

Rules during warm-up:

  • Never send to addresses that haven't engaged in 6+ months during warm-up
  • Monitor spam rates โ€” if above 0.3% (Gmail), pause and investigate
  • Monitor bounce rates โ€” if above 2%, pause and clean the list
  • Send at consistent times (not random spikes)

List Hygiene

A clean list is as important as authentication. Sending to old, inactive, or invalid addresses damages your sender score.

// Clean list before bulk send
async function cleanEmailList(emails: string[]): Promise<string[]> {
  // 1. Remove suppressions (bounces, complaints, unsubscribes)
  const suppressions = await db.emailSuppression.findMany({
    where: { email: { in: emails } },
    select: { email: true },
  });
  const suppressedSet = new Set(suppressions.map(s => s.email));

  // 2. Validate format
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  // 3. Filter inactive users (haven't opened in 12 months)
  const inactive = await db.user.findMany({
    where: {
      email: { in: emails },
      lastEmailOpenAt: { lt: subMonths(new Date(), 12) },
    },
    select: { email: true },
  });
  const inactiveSet = new Set(inactive.map(u => u.email));

  return emails.filter(email =>
    !suppressedSet.has(email) &&
    emailRegex.test(email) &&
    !inactiveSet.has(email)
  );
}

Deliverability Monitoring

MetricGoodWarningCritical
Delivery rate> 98%95โ€“98%< 95%
Bounce rate< 0.5%0.5โ€“2%> 2%
Spam complaint rate< 0.1%0.1โ€“0.3%> 0.3%
Open rate (engaged list)> 25%15โ€“25%< 15%

Tools: Google Postmaster Tools (free, shows Gmail domain reputation), MXToolbox, Mail-Tester (test a specific email), 250ok (enterprise monitoring).


Working With Viprasol

We set up transactional email infrastructure โ€” SPF/DKIM/DMARC, SES configuration, bounce handling, suppression lists, and warm-up schedules โ€” as part of SaaS product builds. Getting email deliverability right at launch is far easier than fixing reputation damage later.

โ†’ Talk to our team about your email infrastructure.


See Also

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Building a SaaS Product?

We've helped launch 50+ SaaS platforms. Let's build yours โ€” fast.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท AI Agent Systems

Add AI automation to your SaaS product?

Viprasol builds custom AI agent crews that plug into any SaaS workflow โ€” automating repetitive tasks, qualifying leads, and responding across every channel your customers use.