Launched:self-serve migrations intoSuperhuman Docs (Coda)
Try it now
01Agent-first
Runs where you already work
Plug it into Claude, ChatGPT or Cursor. Describe the move in plain English; the agent runs it.
02Engineer-led
Our production engine, unlocked
The pipeline our engineers use on managed enterprise migrations — the same code, now something you can drive yourself.
03Pricing
Try 10 pages free, then $1 a page
Credit-based, pay-as-you-go. No scoping call, no quote — sample it on your own docs before you spend anything.
04Sources
NotionSlabConfluenceSoonGoogle DocsSoon
Skip to content

Preserving Email History, Threads & Activity Logs in a CRM Migration

Email threads, timestamps, and activity logs break silently in CRM migrations. This guide covers the platform-specific constraints and process to preserve them.

Abdul Wahab Abdul Wahab · · 26 min read
Preserving Email History, Threads & Activity Logs in a CRM Migration
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

Email threads, logged calls, and activity timelines are the hardest data classes to move between CRMs. Contacts and deals migrate with a CSV upload. Email history does not.

Every major CRM stores email activity differently — as engagement objects, activity records, or synced-but-unexportable mailbox data — and a migration that ignores those differences will silently orphan years of customer context. To preserve email history, you must move it as structured relational data with original timestamps, reply-chain headers (Message-ID, In-Reply-To, References), participant lists, attachments, and multi-object associations intact. Import only subject and body, and you keep text but lose the metadata systems use to order conversations and correlate replies. (rfc-editor.org)

This guide covers the specific technical constraints, platform-by-platform edge cases, and the extraction-to-validation process that keeps email threads, timestamps, and activity associations intact when you move from one CRM to another.

For a broader checklist covering field mapping, pipeline stages, and consent records, see The Ultimate CRM Data Migration Checklist.

Why Email History Is the Hardest Data to Migrate Between CRMs

Email history in a CRM refers to the logged emails, email threads, and associated metadata — timestamps, direction, attachments, sender/recipient — tied to contact, deal, or case records. Unlike structured fields on a contact record, email data is relational, multi-object, and stored in platform-specific formats that have no universal export standard.

Three things make this data class uniquely difficult:

  1. Threading depends on hidden headers. Email threads are held together by Message-ID, In-Reply-To, and References headers embedded in each message per RFC 5322. If these headers are stripped or missing during extraction, replies detach from their parent threads and appear as standalone messages in the target system. Lose the headers, and future replies from external contacts will also start new threads — because the correlation metadata the receiving system needs no longer exists.

  2. Every CRM stores emails differently. Salesforce uses the EmailMessage object linked via ParentId to Cases and RelatedToId to Accounts or Opportunities. HubSpot stores emails as engagement objects associated through a separate Associations API. Pipedrive syncs emails into a mailbox layer visible in the UI but largely inaccessible through standard exports. Dynamics 365 rolls email into activitypointer entities with their own correlation logic. These are not interchangeable formats — they are fundamentally different data models.

  3. CSV imports destroy email relationships. A CSV export flattens multi-object associations into rows and columns. A single email in your CRM might be simultaneously tied to a sender (User), a recipient (Contact), a company (Account), an active deal (Opportunity), and three attached PDFs. A CSV has no mechanism to represent these polymorphic relationships. When you reimport that CSV, the target system cannot reconstruct them. The result: emails appear as disconnected notes, attachments are severed, and the historical timeline of the deal is permanently lost.

CRM activity logs — the chronological records of emails, calls, meetings, tasks, and notes tied to a contact or deal — break for the same structural reasons. Activities are polymorphic: in Salesforce, a single WhoId on a Task can point to a Contact or Lead, while WhatId can point to an Account, Opportunity, or custom object. Each platform uses a different activity model (Salesforce separates Tasks and Events; HubSpot uses unified engagements with subtypes; Dynamics 365 inherits everything from activitypointer). And most CRMs set the activity creator to the importing user, not the original rep who logged the interaction. Mapping between these models is a translation problem, not a copy operation.

Warning

If you do not provide original activity timestamps, the timeline lies. HubSpot explicitly sets new activity dates to the creation or import time when Activity date is omitted. Salesforce stamps CreatedDate at insert time unless audit fields are enabled. Every email in your migration will appear as if it happened today. (knowledge.hubspot.com)

How Each CRM Stores Email Data (And What That Means for Migration)

Before you extract anything, understand how your source system structures email data. That structure dictates what is exportable, what requires API calls, and what might not come out at all.

CRM Email Storage Model Threading Mechanism Export Method Key Constraint
Salesforce EmailMessage with ParentId, RelatedToId, EmailMessageRelation junction Message-ID, In-Reply-To, References in Headers field Bulk API 2.0 / SOQL Headers field can be blank on legacy records; audit fields required for timestamp preservation
HubSpot Email engagements via CRM Associations API (v3: /crm/v3/objects/emails) No native thread ID exposed via API Engagements API v3 Emails are create-only — cannot be updated after import
Pipedrive Synced emails in a mailbox layer; activities are separate objects No documented threading structure Mailbox API (/v1/mailbox/mailMessages) Native export excludes synced emails entirely
Dynamics 365 Email activity entity inheriting from activitypointer InReplyTo, ConversationIndex, correlation method Dataverse API ModifiedOn is read-only; resets to import time
Zoho CRM Emails module linked to contacts/deals Subject-line based grouping Zoho API Upsert preserves original timestamps
Freshsales Email activities stored as sales_activity objects linked to contacts and deals No native thread ID; subject-based grouping Freshsales API v2 (/api/activities) created_at resets to import time unless explicitly set in payload
Copper Emails synced from Gmail via Google Workspace integration Gmail threadId preserved if synced via native integration Copper API (/v1/activities/email) Non-Gmail sources have no threading metadata; API exposes limited history

Salesforce: The Most Structured Model — With Caveats

Salesforce's EmailMessage object is the most structured email storage model among major CRMs. Each email is a discrete record with fields for Subject, HtmlBody, TextBody, FromAddress, ToAddress, MessageDate, Headers, and IsIncoming. The EmailMessageRelation junction object connects each email to every participant (From, To, CC, BCC). (developer.salesforce.com)

Three migration-critical details:

Timestamp preservation requires audit fields. Standard API users cannot write to system fields like CreatedDate, CreatedById, or LastModifiedDate. You must enable Create Audit Fields upon Record Creation in Salesforce Setup. Once enabled, your API payload can explicitly set the historical timestamp:

{
  "Subject": "Re: Q3 Contract Renewal",
  "TextBody": "Let's proceed with the redlines.",
  "CreatedDate": "2021-08-14T15:30:00Z",
  "CreatedById": "005Dn000001XXXXX",
  "MessageDate": "2021-08-14T15:30:00Z"
}
Warning

The "Create Audit Fields" permission only applies during the initial POST of a record. You cannot update the CreatedDate of an existing record. If a historical email is imported with the wrong timestamp, it must be hard-deleted and re-inserted.

Do not set Status to Sent during initial insert. Setting Status to 3 (Sent) makes the record read-only immediately, blocking your ability to populate EmailMessageRelation records or attach files via ContentDocumentLink. Load the message body and metadata first, add relations and attachments, then update Status as the final step.

The Headers field is frequently blank on older records. Records created before Lightning Threading was enabled, or emails logged manually via BCC, often have no Message-ID or In-Reply-To data. Audit your source before extraction:

SELECT Id, ParentId, Subject, Headers, MessageDate
FROM EmailMessage
WHERE Headers = null

If a large portion of records return empty headers, thread reconstruction in the target will fall back to subject-line matching and timestamp proximity — less reliable, but often necessary. See the fallback algorithm in Step 3 below.

One more edge case: if the source org uses Einstein Activity Capture with Sync Email as Salesforce Activity turned off, email data lives on Activity Platform Hyperforce rather than the main Salesforce instance. Confirm where your emails actually reside before you start. (help.salesforce.com)

HubSpot: Create-Only Emails and the Timestamp Trap

HubSpot stores emails as engagement objects associated to contacts, companies, and deals through the Associations API. On import via the v3 /crm/v3/objects/emails endpoint, you set properties like hs_timestamp, hs_email_direction, hs_email_subject, and hs_email_text, then create associations separately.

Two constraints catch most migration teams:

  • Emails are create-only. HubSpot's documentation confirms that emails, meetings, notes, and tasks cannot be updated through imports. If you get the timestamp or association wrong, your only fix is to delete and re-import. There is no in-place correction. (knowledge.hubspot.com)
  • The Created by field is system-set. Every migrated email is attributed to the importing user, not the original sender. The Activity date field is technically optional, and omitting it causes every activity to be dated at import time — collapsing years of chronological history into a single day.

For threading, HubSpot has no native thread ID exposed via API. To preserve conversational context, populate the hs_email_headers property with a stringified JSON object containing Message-ID, In-Reply-To, and References values. Omit them, and HubSpot treats every reply as a brand-new, disconnected email.

HubSpot enforces both burst and daily rate limits on engagement creation. The v3 API allows up to 100 requests per 10 seconds per token, with a daily cap that scales by portal tier. High-volume email migrations — 50,000+ engagements — will hit daily limits within hours. Plan for multi-day load windows or request a temporary rate limit increase from HubSpot support before starting.

CSV imports cannot reconstruct threaded conversations, backdate historical timestamps on individual messages, or migrate attachments. The API is mandatory for email history. For a broader look at preparing your HubSpot environment, see our HubSpot Service Hub migration checklist.

Pipedrive: The Export Gap That Surprises Everyone

Pipedrive's native export covers leads, deals, people, organizations, products, activities, projects, notes, and files — but not synced emails. The emails visible on contact and deal timelines are stored in a mailbox layer that Pipedrive's standard export tool simply skips.

Extraction requires the Mailbox API endpoints (GET /v1/mailbox/mailMessages), and even then, email attachments require separate file download calls. Some metadata — headers, threading — may be incomplete depending on how the email was synced (full email sync vs. BCC dropbox). Emails marked as private are subject to additional visibility restrictions: the API will refuse to return them unless the migration script authenticates as the owning user, or an organization administrator explicitly overrides mailbox visibility settings before extraction.

Pipedrive's API rate limit is approximately 80 requests/second on Professional and above tiers (token bucket model). For organizations with 20,000+ synced emails, this means extraction alone takes several hours and requires separate download calls per attachment.

This is a practical reality: the data your reps see on their deal timelines is not the data you can programmatically export without specific API work. Extracting 20,000+ emails from Pipedrive typically requires multiple migration passes because the API refuses to return emails marked as private without per-user authentication.

Dynamics 365: Two Distinct Problems

Dynamics 365 stores emails as activity entities and presents them on the Timeline control sorted by ModifiedOn date. This creates two distinct migration problems.

The ModifiedOn reset. ModifiedOn is a read-only, system-managed field. When you import historical email activities, every record's ModifiedOn resets to the import timestamp, scrambling timeline order. The established workaround is a custom plugin registered on the Pre-Operation Create and Update events that overwrites ModifiedOn with the original value from a staging field. For closed activities, a status update after import triggers another ModifiedOn reset, requiring a second plugin registration on statecode changes.

Threading correlation logic. Dynamics server-side sync checks InReplyTo first, then tracking token, then ConversationIndex, then smart matching. Microsoft recommends leaving InReplyTo correlation as the default because it is accurate and avoids false positives. Smart matching is a fallback and can mis-correlate. If a reply client strips InReplyTo after a subject edit, the thread can break even when the human reader sees the same conversation. (learn.microsoft.com)

Freshsales: Reset Timestamps and Limited Threading

Freshsales stores email activity as sales_activity objects linked to contacts and deals. The created_at field resets to import time unless it is explicitly included in the API payload — behavior identical to HubSpot's hs_timestamp omission trap. Threading relies on subject-line grouping rather than RFC 5322 headers, which means that replies with edited subjects (common in long sales cycles) will appear as separate conversations even when the threading context is logically continuous.

Freshsales API v2 (/api/activities) supports batch creation but does not expose a thread ID or header passthrough field. For migrations targeting Freshsales, the recommended approach is to write emails in strict chronological order grouped by contact, and to include the original sent_at timestamp explicitly in every payload.

Copper: Gmail-Native Threading, Non-Gmail Gaps

Copper's email activity is derived from Gmail via Google Workspace integration, which means that when source emails originated in Gmail with intact threadId metadata, Copper can preserve threading natively on import. For emails from non-Gmail sources (Outlook, legacy SMTP), there is no threading metadata in Copper's API — the threadId field will be null, and the Copper API (/v1/activities/email) does not provide a passthrough field for Message-ID or In-Reply-To.

For organizations migrating into Copper from a non-Gmail source CRM, the practical outcome is that email history imports as a flat list of activities without conversational grouping. If preserving thread structure is a requirement, post-migration thread reconstruction must be handled by writing emails into Gmail directly via the Gmail API (with correct References and In-Reply-To headers), allowing Copper's Gmail sync to ingest the reconstructed threads.

Gmail and Microsoft 365: When the Mailbox Is the Source of Truth

Sometimes the CRM is not the primary source of email history — the mailbox provider is. Gmail treats threads as first-class objects and supports adding migrated messages to existing threads if you supply the target threadId, set References and In-Reply-To per RFC 5322, and keep the Subject aligned. Gmail also supports change capture through watch and history, making delta sync practical during staged cutovers. (developers.google.com)

Microsoft Graph exposes internetMessageId, internetMessageHeaders, conversationId, conversationIndex, full MIME retrieval via /$value, and attachment APIs. The less obvious trap is ID stability: default Graph message IDs are not immutable. If your process keys on Graph id, request Prefer: IdType="ImmutableId" consistently to avoid references breaking across folder moves. (learn.microsoft.com)

What Data Preserves a Real Email Thread?

A real email-thread migration preserves both message content and reply context. The fields below are the ones vendors themselves use for threading, correlation, and timeline placement.

  • Stable source key: the source message ID and the mailbox or tenant context that makes it unique. In Microsoft 365, request immutable IDs if you need a stable Graph identifier across folder moves.
  • Reply-chain metadata: Message-ID, In-Reply-To, and References, plus provider-specific fields like Gmail threadId or Outlook conversationId and conversationIndex.
  • Participants: From, To, Cc, Bcc, display names, and direction. In Salesforce, recipient relationships are modeled separately in EmailMessageRelation — easy to miss if you only extract the parent email row. (developer.salesforce.com)
  • Original timestamps: sent, received, created, last modified, and imported-at times must remain distinct. Collapsing them corrupts reporting and chronology.
  • Bodies in the right format: HTML and plain text where possible. Microsoft Graph returns full MIME with /$value; Gmail supports import/insert semantics closer to real mailbox data than a flat export.
  • Attachments and parent links: filename, content type, binary blob, and the parent message reference. HubSpot's retroactive logging does not capture non-inline attachments.
  • CRM associations: which contact, company, deal, ticket, case, or custom object the activity belonged to. This relationship is what makes timeline history usable inside the target CRM.

A practical canonical export for email history looks more like this than a CSV row:

sourceSystem: microsoft365
mailbox: rep@company.com
sourceMessageId: <abc123@example.com>
sourceThreadId: AAQkAG...
inReplyTo: <prev@example.com>
references:
  - <root@example.com>
  - <prev@example.com>
sentAt: 2026-03-14T16:21:09Z
direction: outbound
subject: Re: Renewal terms
from:
  - name: AE
    email: rep@company.com
to:
  - name: Buyer
    email: buyer@example.com
attachments:
  - fileName: quote.pdf
    contentType: application/pdf
crmLinks:
  - object: contact
    sourceId: c_44
  - object: deal
    sourceId: deal_9281

If you do not have something like this intermediate representation, you are not migrating email history. You are loading text.

How to Preserve Email Threads During a CRM Migration

Step 1: Audit the Source Email Data

Before extraction, quantify what you are working with:

  • Total email count by record type (contact, deal, case)
  • Header completeness — what percentage of emails have Message-ID, In-Reply-To, References populated?
  • Attachment volume — total file count and aggregate size
  • Private/visibility flags — emails marked private in Pipedrive or Salesforce may require elevated permissions or different extraction endpoints
  • Date range — how far back does the email history go, and what is the oldest record with intact metadata?
  • Where the data actually lives — is it in the CRM, the mailbox provider, a sync layer, or a mix of all three? Salesforce with Einstein Activity Capture is the clearest example: captured email can live in standard EmailMessage records or off-core on Hyperforce depending on EAC settings.
  • Duplicate contact records with overlapping email history — if the source CRM has multiple contact records for the same person (a common data quality issue), their email histories may need to be merged before or during migration. See the deduplication section below.

This audit determines whether you can reconstruct threads programmatically or need to fall back to subject-line and timestamp-based grouping.

Step 2: Extract via API, Not CSV

API > JSON > CSV — and email data is the reason why. CSV exports flatten relational structures and strip the metadata that holds threads together.

  • Salesforce: Use Bulk API 2.0 to query EmailMessage with fields including Id, ParentId, RelatedToId, Subject, HtmlBody, TextBody, FromAddress, ToAddress, MessageDate, Headers, IsIncoming, and Status. Extract EmailMessageRelation records separately to capture CC/BCC participants.
  • HubSpot: Use the v3 emails endpoint (/crm/v3/objects/emails) with the associations parameter to retrieve linked contacts, companies, and deals in a single call. Include properties: hs_timestamp, hs_email_direction, hs_email_subject, hs_email_text, hs_email_html.
  • Pipedrive: Use the Mailbox API. Standard entity exports will not include email data. Plan for ~80 requests/second on Professional and above tiers, and build in separate download calls for attachments.
  • Gmail / Microsoft 365: When the mailbox is the source of truth, use Gmail's threads API or Microsoft Graph's message endpoint with MIME retrieval. These expose full headers, conversation IDs, and attachment contents that CRM-side exports may lack.

Maintain a mapping table (source_id → target_id) for every record. This table is the backbone of the entire migration — without it, you cannot re-associate emails to the correct contacts, deals, or cases in the target system.

Step 3: Transform Threading Metadata for the Target Model

The transformation layer must translate the source system's threading model into the target's. Three scenarios, in descending order of reliability:

Scenario A — Source has full RFC 5322 headers → Target supports header passthrough Carry Message-ID, In-Reply-To, and References through directly. This is the ideal case (e.g., Salesforce EmailMessage.Headers to Salesforce EmailMessage.Headers, or to HubSpot hs_email_headers). No reconstruction required.

Scenario B — Source has full headers → Target has no header support (e.g., Freshsales, Copper non-Gmail) Group emails into threads using In-Reply-To chains, then write them in chronological order as sequential engagement records associated to the same parent object. The thread structure exists in your intermediate representation but cannot be expressed in the target's data model.

Scenario C — Source has no headers → Any target (fallback reconstruction) Apply the following composite-key matching algorithm:

function buildThreadKey(email):
  normalizedSubject = stripPrefixes(email.subject)
    # Remove "Re:", "Fwd:", "FW:", "AW:", "RE:", case-insensitive, recursively
  participantSet = sort([email.fromAddress] + email.toAddresses).join("|")
  return (normalizedSubject, participantSet, contactId)

function assignThreads(emails, windowHours=72):
  sort emails by sentAt ascending
  threads = {}
  for email in emails:
    key = buildThreadKey(email)
    if key in threads:
      lastSeen = threads[key].lastTimestamp
      if (email.sentAt - lastSeen) < windowHours * 3600:
        email.threadId = threads[key].threadId
        threads[key].lastTimestamp = email.sentAt
      else:
        # Gap too large — treat as new thread
        email.threadId = newId()
        threads[key] = {threadId: email.threadId, lastTimestamp: email.sentAt}
    else:
      email.threadId = newId()
      threads[key] = {threadId: email.threadId, lastTimestamp: email.sentAt}
  return emails

The 72-hour window is a heuristic. Reduce it for high-volume accounts where subject collisions are likely; increase it for low-frequency executive correspondence. This algorithm is not deterministic — expect 3–8% misattribution on records with generic subjects ("Following up", "Introduction") at high email volumes.

When migrating into Salesforce's EmailMessage object, threading uses the ReplyToEmailMessageId field. This requires sequential loading: insert the parent email first, capture its new Salesforce ID, and inject that ID into the child email's payload.

Step 4: Handle Deactivated and Legacy Users

Your historical CRM data is full of emails sent by reps who left the company years ago. Target CRMs typically reject API payloads assigned to users who do not exist or are marked inactive — returning a 400 Bad Request or 404 Not Found and dropping the record entirely.

Three strategies:

  1. Legacy admin account. Create a single active user named "Legacy Data" or "Archived User." Map all historical activities from departed reps to this account. Preserves the data but sacrifices attribution.
  2. Temporary provisioning. Create active accounts for all departed reps, run the migration with exact attribution, then deactivate those users after validation. Requires temporary seat expansion during the migration window.
  3. Inactive user mapping. Some CRMs allow assigning records to inactive users with specific permissions enabled. Salesforce supports this via the Update Records with Inactive Owners permission. This is the cleanest option where available.

Step 5: Handle Duplicate Contacts and Overlapping Email History

Source CRMs frequently contain duplicate contact records — two or more records representing the same person, each with a subset of the total email history. If those duplicates are merged in the target CRM, their email timelines must be merged too. If they are not deduplicated, emails associated to the "wrong" duplicate will be invisible on the surviving record's timeline.

The deduplication decision must be made before the email migration runs, not after. The recommended sequence:

  1. Identify duplicates in the source using email address as the primary match key. Secondary keys: normalized name + company domain.
  2. Designate a winner record per duplicate cluster. Capture the mapping: [loser_contact_id_1, loser_contact_id_2] → winner_contact_id.
  3. During email extraction, associate all emails from loser records to the winner's source_id in your mapping table.
  4. Load emails once against the winner's target_id. Do not load emails for loser records — the target record does not exist.

If deduplication is deferred to post-migration, emails associated to merged-away records become orphans. Retroactively re-associating thousands of orphaned activities is expensive and error-prone.

Step 6: Load in Dependency Order

Email records depend on their parent objects existing first. The correct load sequence:

Accounts/Companies → Contacts → Deals/Opportunities → Cases/Tickets → Email Messages → Attachments → Activity Associations

For each email record, resolve the source_id of the parent contact, deal, or case to its target_id using your mapping table. If you skip this step, emails load into the database but are invisible on customer timelines — orphaned activities that exist in the system but serve no one.

For Salesforce EmailMessage records specifically: insert the message body first with Status unset, create EmailMessageRelation and ContentDocumentLink records, then update Status to 3 (Sent) as the final step to lock the record.

Step 7: Migrate Attachments and Inline Images

Email attachments are one of the highest-friction points in a CRM migration. Two types must be handled:

  • Explicit attachments: PDFs, DOCX files, and other files attached to the email.
  • Inline images: Base64-encoded images embedded in the HTML body — signature logos, inline screenshots.

The process is a multi-step API orchestration:

  1. Extract: Download the binary file from the source CRM.
  2. Upload: Upload to the target CRM's file storage (e.g., Salesforce ContentVersion, HubSpot Files API). If you are moving between these specific platforms, our Salesforce to HubSpot migration guide covers the attachment workflow in detail.
  3. Associate: Link the new File ID to the specific Email Activity ID.
  4. Rewrite HTML: For inline images, parse the email's HTML body, locate old src URLs, and replace them with the target URLs before uploading the email record.

This process is highly susceptible to API rate limits. Downloading and re-uploading large attachment libraries consumes significant API quota and bandwidth. Plan for this in your migration timeline.

Step 8: Build an Error Handling and Retry Strategy

API failures during email migration are not edge cases — they are expected. Rate limit errors (429 Too Many Requests), transient server errors (503 Service Unavailable), and payload validation failures (400 Bad Request) all occur in production migrations.

A correct retry strategy by error type:

HTTP Status Cause Strategy
429 Rate limit exceeded Exponential backoff: wait 2^n seconds (n = retry attempt, cap at 60s). Read the Retry-After header if present — HubSpot and Salesforce both return it.
503 / 504 Transient server error Retry up to 3 times with 5-second fixed delay. If all retries fail, write to dead-letter queue.
400 Payload validation failure Do not retry. Log the full request payload and error response. Validate field types and required fields before next batch.
404 Parent record not found Do not retry. The parent object (contact, deal) either failed to load or was not mapped. Check mapping table.
409 Duplicate record conflict Log and skip. Indicates the record already exists in the target — common on re-runs after partial failures.

Maintain a dead-letter queue — a structured log of every record that failed after all retries, with the source ID, error code, and full response body. After the migration completes, the dead-letter queue is your remediation list. Any migration without one leaves failed records undetected.

For partial-failure rollback: because most CRM APIs do not support transactions across multiple objects, true rollback is not practical at scale. Instead, design the migration to be idempotent — each record load checks whether the record already exists in the target (using the source_id stored in a custom field) before creating it. This allows safe re-runs after failure without duplicate creation.

Step 9: Run the Delta Sync

Migrating hundreds of thousands of historical emails takes time, but your sales team cannot pause while the data moves. Structure the migration in two phases:

  1. Initial bulk load: Extract and import all historical data up to a specific timestamp. This captures the vast majority of data and can run in the background over several days.
  2. Delta sync: A targeted migration executed immediately before go-live, querying strictly for records created or modified after the bulk load's cutoff timestamp.

Gmail's watch and history model and Microsoft Graph's change notifications both support this incremental catch-up pattern natively. (developers.google.com) For CRM-to-CRM migrations, filter the source API by LastModifiedDate or equivalent.

API Rate Limits by Platform

Rate limits are the binding constraint on migration throughput for email-heavy projects. The table below consolidates per-platform limits relevant to email and activity migration.

Platform Rate Limit Notes
Salesforce Bulk API 2.0 150 MB/day ingest per org; concurrent job limit of 5 Bulk jobs bypass per-request API limits; use for volumes above 2,000 records
HubSpot Engagements API v3 100 requests/10 seconds per token; daily cap varies by tier Daily cap for Professional: ~500,000 API calls/day across all integrations
Pipedrive Mailbox API ~80 requests/second (token bucket, Professional+) Attachment downloads count as separate requests
Dynamics 365 Dataverse API 6,000 requests/5 minutes per user; 1,200 requests/5 minutes for service principals Batch operations (up to 1,000 rows per batch) reduce request count significantly
Freshsales API v2 1,000 requests/hour per token Batch endpoint available but limited to 50 records per call
Microsoft Graph (M365) 10,000 requests/10 minutes per app per tenant MIME retrieval (/$value) counts separately; use $batch to combine up to 20 requests
Gmail API 250 quota units/second per user; 1,000,000,000 quota units/day Insert operations cost 25 units; use domain-wide delegation for multi-mailbox migrations

For migrations exceeding these limits, the standard approach is to distribute load across multiple API tokens (where the platform allows), use batch endpoints wherever available, and schedule bulk operations during off-peak hours when shared API quotas are less contested.

Migrating email history across systems or jurisdictions is not purely a technical operation. Email records frequently contain personal data — names, contact details, the content of private business communications — and several compliance obligations apply.

Lawful basis for processing. Before migrating email history into a new CRM, confirm that your organization's lawful basis for processing the personal data in those records (typically legitimate interest or contractual necessity for B2B) extends to the new system. If the migration moves data to a different legal entity, data processor, or jurisdiction (e.g., from an EU-hosted CRM to a US-hosted one), a transfer mechanism under GDPR Chapter V is required — typically Standard Contractual Clauses (SCCs) or an adequacy decision.

Data minimization. GDPR Article 5(1)(c) requires that personal data be adequate, relevant, and limited to what is necessary. A migration is an appropriate moment to apply this principle: do not migrate email records that have no active business purpose. Marketing engagement exhaust (open/click events), auto-generated sync notifications, and emails on permanently closed-lost deals with no ongoing relationship are candidates for exclusion.

Retention periods. If your source CRM contains email records beyond your documented retention period, those records should not be migrated — they should be deleted. Migrating out-of-retention data into a new system restarts its visibility without resetting the underlying compliance obligation.

PHI and BAA requirements. If any email records contain Protected Health Information (PHI) under HIPAA, a Business Associate Agreement must be in place with the target CRM vendor before migration begins. This applies to both the CRM platform and any intermediate tooling (migration scripts, staging databases) that handles the data.

Right to erasure. If any contacts have submitted erasure requests (GDPR Article 17) that were actioned in the source CRM, verify that those records are excluded from the migration extract before the migration runs — not retroactively after load.

Full Fidelity vs. Tiered Retention

Not every email needs to land in the new CRM with full fidelity. Before migrating, answer three questions:

  1. How far back does email history need to be readable in the new system? Full fidelity for the last 2 years and a searchable archive for everything older is often the right balance. It is significantly cheaper than migrating 10 years of email at full fidelity, and it matches how reps actually use the data.
  2. Where does the archive live? A read-only instance of the old CRM (kept active for 3–6 months post-cutover), a data warehouse, or a flat export in cold storage are all valid options with very different costs.
  3. What are your compliance requirements? Regulated industries may need to retain email records for specific periods with full audit trails. PHI requires a BAA with the target vendor before migration.

Usually worth migrating:

  • Email threads on open deals, active cases, and live renewals
  • Recent human-authored notes, calls, meetings, and tasks
  • Attachments referenced in active customer conversations
  • Shared mailbox history the team actively works from

Usually better archived:

  • Marketing open and click exhaust
  • Auto-generated sync chatter
  • Old closed-lost deal mail
  • Duplicate activities with empty bodies
  • Mail with no surviving customer association

If your source CRM has 10+ years of email data, migrating everything at full fidelity may not be worth the cost or timeline. Running the old system in read-only mode for 3–6 months gives reps access to historical context while the new system accumulates fresh activity data. For more on this approach, see Why Running Two CRMs in Parallel Beats a Hard Cutover.

Common Failure Modes (And How to Avoid Them)

Failure Mode What Happens Prevention
Timestamps default to import date Every email appears as sent/received today, destroying chronological order Set the activity date/timestamp field explicitly; validate in sample migration
Threads split into standalone emails A 12-message conversation appears as 12 unrelated messages Extract and preserve Message-ID/In-Reply-To headers; reconstruct thread chains before loading
Emails orphaned from parent records Emails exist in the target but are not associated to any contact, deal, or case Load parent objects first; maintain source_id → target_id mapping; validate association counts
Attachments missing or detached Email records arrive but file attachments are gone or linked to wrong records Extract attachments separately; re-associate via ContentDocumentLink (Salesforce) or inline on creation (HubSpot)
Private emails excluded from export Emails marked private do not appear in API responses Use elevated permissions or per-user API tokens; audit against UI-visible counts
Owner attribution lost All emails show the migration service account as creator Set owner/assignee fields explicitly where the target API supports it; document the limitation where it does not
Future replies start new threads Post-migration replies from external contacts detach from history Preserve Message-ID/References headers so the receiving system can correlate new replies with migrated threads
Duplicate contacts produce orphaned emails Emails on merged-away contact records become invisible after deduplication Resolve duplicates before migration; map loser contact IDs to winner IDs in the mapping table
Rate limit failures drop records silently API returns 429; migration script moves on without retrying; records are lost Implement exponential backoff; maintain dead-letter queue; validate final counts against source
Out-of-retention data migrated Personal data beyond documented retention periods is moved into the new system, creating compliance exposure Apply retention filters during extraction; exclude records beyond the documented retention period

Validating Email History After Cutover

Validate at five levels. Anything less is a smoke test, not a migration QA plan.

  • Count parity: Compare totals by activity type, owner, date band, and associated object. Account for any intentional exclusions (e.g., emails older than a cutoff date).
  • Chronology parity: Spot-check real threads to confirm sent order, timezone normalization, and original activity dates. Open a contact with 20+ historical emails and verify the timeline reads correctly.
  • Association parity: Verify that sampled emails are attached to the right contact and the right deal, ticket, or case — not just one of them.
  • Render parity: Open HTML bodies, inline images, and attachments on a random sample from each source system. Broken inline images are easy to miss in bulk validation.
  • Live reply test: Send an email from the target CRM, reply from an external mailbox, and confirm the response lands on the correct record and threads with the migrated history. Gmail, Outlook, and Dynamics all rely on message headers for this behavior — not visual subject similarity. (developers.google.com)
Tip

Run one validation round on a sample migration and another on the final load. Catching thread breaks in a sample is much cheaper than cleaning up a broken timeline after cutover.

How Long Does a CRM Email Migration Take?

Keep two clocks separate: the duration of the data migration itself, and the duration of your internal CRM replatforming project. Start scoping at least 3–4 months before an internal deadline — rebuilding workflows, training staff, and configuring a new CRM takes time. The data migration is a different workstream.

The migration itself is faster than most teams expect. Most migrations complete within 1–5 business days, depending on data volume, relational complexity, and API rate limits.

  • Under 10,000 records: Often completes in under 24 hours.
  • 10,000–50,000 records: 3–7 business days, depending on attachment volume and threading complexity.
  • 50,000+ records: Custom engagement with an initial bulk migration followed by delta syncs. Larger engagements run 7–13+ business days. Full migrations typically run over a weekend to minimize disruption.

Email history is the object type that adds the most time to a CRM migration. A 5-year-old CRM with 10,000 contacts commonly has 100,000–500,000 associated email records — the ratio of emails to contacts is typically 10:1 to 50:1 in active sales organizations, based on the distribution of activity across open and closed deal timelines. Target-side API rate limits are usually the binding constraint: HubSpot has burst and daily rate limits on engagement creation, Salesforce allows larger batch sizes but enforces concurrent job limits, and Pipedrive's token bucket refills at roughly 80 tokens/second on Professional tiers.

These timelines cover the data migration work — not a broader CRM implementation or replatforming project.

What ClonePartner Handles for Email and Activity Migrations

We have migrated email history, threads, and activity logs across every major CRM pairing — including the edge cases where source APIs do not expose the data you would expect. Each project gets a dedicated migration engineer (not a ticket queue), and only that engineer can view your data.

Our process for email-heavy migrations:

  1. Connect accounts and audit the source email data — volume, header completeness, attachment size, API constraints
  2. Build the migration checklist with field-level mapping for email properties, threading metadata, and association targets
  3. Run unlimited sample migrations until email threads render correctly in the target system with accurate timestamps, associations, and attachments
  4. Execute the full migration with multi-stage validation: checksums, record-count parity, field-level validation, and referential integrity checks
  5. Delta sync to capture any emails logged between the bulk migration and cutover

Security is enforced throughout. ClonePartner operates under SOC 2 Type II and ISO 27001 safeguards, with AES-256 encryption at rest and TLS 1.2+ in transit. Staging databases auto-purge no later than 30 days after the project closes.

One fixed price per project, starting at $500 — no hourly billing, no hidden fees, no per-record metering.

Frequently Asked Questions

Can you migrate email history using a CSV import?
No. CSV imports flatten relational data, breaking the links between emails, contacts, and deals. They strip threading headers (Message-ID, In-Reply-To, References), reset creation timestamps to the import date, and sever file attachments. API-based extraction and loading is required to preserve email history.
Why do email threads break during CRM migration?
Email threads are held together by Message-ID, In-Reply-To, and References headers embedded in each message. If these headers are stripped during extraction or not mapped to the target system's threading model, replies detach from their parent threads and appear as standalone messages. Future replies from external contacts will also start new threads because the correlation metadata is gone.
Does HubSpot support importing historical emails?
HubSpot does not support importing historical emails via its standard CSV import tool. You must use the Engagements API (v3 /crm/v3/objects/emails endpoint) to create email records with backdated timestamps and associations. Emails are create-only — they cannot be updated after import, so timestamps and associations must be correct on the first pass.
Can you export emails from Pipedrive?
Pipedrive's native export tool does not include synced emails. You must use the Mailbox API (/v1/mailbox/mailMessages) to extract email data, and attachments require separate download calls. Emails marked as private may have additional access restrictions via the API.
How long does it take to migrate email history between CRMs?
The data migration itself typically takes 1–5 business days, depending on email volume, API rate limits, and threading complexity. Small datasets under 10,000 records can finish in under 24 hours. Larger email-heavy migrations may run 7–13+ business days with bulk migration plus delta syncs. These timelines cover the migration work, not a broader CRM replatforming project.

More from our Blog

HubSpot Service Hub Migration Checklist
Checklist/Hubspot Service Hub

HubSpot Service Hub Migration Checklist

A technical guide on migrating conversation history from Front to HubSpot Service Hub, covering API extraction, data mapping, attachment handling, and thread reconstruction.

Tejas Mondeeri Tejas Mondeeri · · 6 min read