Guide

Export Email Data to Salesforce

Your inbox contains relationship data that Salesforce never sees — sender domains, CC patterns, reply frequency, meeting context. This guide shows how to export email and contact data from Nylas CLI and import it into Salesforce as Leads, Contacts, Accounts, and Tasks.

Why sync email data to Salesforce

Email reveals relationships that CRM data entry misses. Every email exchange is an implicit activity log — who talked to whom, when, about what, and how often. Salesforce captures none of this unless someone manually logs it, and manual logging has a compliance rate near zero for most sales teams.

Automatically syncing email data to Salesforce gives you three things. First, auto-logged activities: every email becomes a Task record linked to the right Contact or Lead, giving managers visibility into deal engagement without asking reps to log calls. Second, enriched records: email signatures contain phone numbers, job titles, and company names that fill in blank CRM fields. Third, relationship intelligence: CC patterns and reply frequency reveal who the real decision-makers are, which accounts are going cold, and where single-threaded risk exists.

The Nylas CLI makes the export side trivial. One command gives you structured JSON for emails or contacts. The rest of this guide focuses on mapping that data to Salesforce objects and getting it in.

Export from Nylas CLI

Start by exporting your email and contact data as JSON. These two commands give you everything you need:

# Export recent emails (sender, subject, date, body snippet)
nylas email list --json --limit 500 > emails.json

# Export contacts (name, email, phone, company)
nylas contacts list --json --limit 500 > contacts.json

# Preview the structure
cat contacts.json | jq '.[0]'

The email export includes from, to, cc, subject, date, and body fields. The contact export includes given_name, surname, emails, phone_numbers, and company_name.

For a single email with full body content, use the read command:

# Read a specific email by ID
nylas email read abc123def --json | jq '{
  from: .from[0],
  to: .to,
  subject: .subject,
  date: .date,
  body: .body
}'

Salesforce object model mapping

Salesforce organizes data into standard objects. Here is how Nylas CLI output maps to each one:

  • Contact — a person you have a relationship with. given_nameFirstName, surnameLastName, emails[0].emailEmail, phone_numbers[0].numberPhone.
  • Lead — a prospect not yet qualified. Same name and email mapping as Contact, plus company_nameCompany.
  • Account — a company. Derive from the email domain: domain → Name (or Website).
  • Task — an activity record. Map email subject → Subject, date → ActivityDate, and set Type to "Email"and Status to "Completed".

The key relationship: each Contact belongs to an Account (via AccountId), and Tasks are linked to a Contact or Lead via WhoId. When you import, create Accounts first, then Contacts, then Tasks.

CSV import via Data Loader

The simplest way to get data into Salesforce is CSV import using Data Loader or the built-in Data Import Wizard. Use jq to transform Nylas CLI output into Salesforce-compatible CSV format.

Contacts CSV

# Transform contacts.json into Salesforce Contact CSV
cat contacts.json | jq -r '
  ["FirstName","LastName","Email","Phone","Title","MailingStreet","MailingCity","MailingState","MailingPostalCode"],
  (.[] | [
    (.given_name // ""),
    (.surname // ""),
    ((.emails // [])[0].email // ""),
    ((.phone_numbers // [])[0].number // ""),
    (.job_title // ""),
    ((.physical_addresses // [])[0].street_address // ""),
    ((.physical_addresses // [])[0].city // ""),
    ((.physical_addresses // [])[0].state // ""),
    ((.physical_addresses // [])[0].postal_code // "")
  ])
  | @csv' > salesforce_contacts.csv

echo "Wrote $(wc -l < salesforce_contacts.csv) rows to salesforce_contacts.csv"

Leads CSV from email senders

If you do not have contacts but have email data, extract leads directly from email senders:

# Extract unique senders from emails and format as Salesforce Lead CSV
cat emails.json | jq -r '
  # Deduplicate by email address
  [.[] | .from[0] | select(.email != null)]
  | group_by(.email)
  | map(.[0])
  | map({
      email: .email,
      name: (.name // ""),
      domain: (.email | split("@")[1])
    })
  | . as $senders
  | ["FirstName","LastName","Email","Company","LeadSource"],
    ($senders[] | [
      (.name | split(" ")[0] // ""),
      (.name | split(" ")[1:] | join(" ") // ""),
      .email,
      .domain,
      "Email"
    ])
  | @csv' > salesforce_leads.csv

echo "Wrote $(wc -l < salesforce_leads.csv) rows to salesforce_leads.csv"

Email activities as Tasks CSV

# Transform emails into Salesforce Task CSV
cat emails.json | jq -r '
  ["Subject","ActivityDate","Status","Priority","Type","Description","WhoId_Email__c"],
  (.[] | [
    .subject,
    (.date | split("T")[0]),
    "Completed",
    "Normal",
    "Email",
    (.body[:200] // ""),
    (.from[0].email // "")
  ])
  | @csv' > salesforce_tasks.csv

echo "Wrote $(wc -l < salesforce_tasks.csv) rows to salesforce_tasks.csv"

To import these CSVs, open Salesforce → Setup → Data Import Wizard (for Leads and Contacts) or install Salesforce Data Loader for bulk operations. Map the CSV columns to Salesforce fields, choose "Upsert" by Email to avoid duplicates, and run the import.

REST API import

For programmatic imports, use the Salesforce REST API. You need a Connected App with OAuth2 credentials and an access token. The examples below use curl to create records.

Authenticate

# Get an access token using the username-password flow
# Replace with your Connected App credentials
SF_TOKEN=$(curl -s https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=password" \
  -d "client_id=$SF_CLIENT_ID" \
  -d "client_secret=$SF_CLIENT_SECRET" \
  -d "username=$SF_USERNAME" \
  -d "password=$SF_PASSWORD$SF_SECURITY_TOKEN" \
  | jq -r '.access_token')

SF_INSTANCE=$(curl -s https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=password" \
  -d "client_id=$SF_CLIENT_ID" \
  -d "client_secret=$SF_CLIENT_SECRET" \
  -d "username=$SF_USERNAME" \
  -d "password=$SF_PASSWORD$SF_SECURITY_TOKEN" \
  | jq -r '.instance_url')

echo "Authenticated to $SF_INSTANCE"

Create a Contact

# Create a Salesforce Contact from Nylas contact data
CONTACT=$(cat contacts.json | jq '.[0]')

curl -s -X POST "$SF_INSTANCE/services/data/v59.0/sobjects/Contact" \
  -H "Authorization: Bearer $SF_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(echo $CONTACT | jq '{
    FirstName: (.given_name // ""),
    LastName: (.surname // "Unknown"),
    Email: ((.emails // [])[0].email // ""),
    Phone: ((.phone_numbers // [])[0].number // ""),
    Title: (.job_title // "")
  }')" | jq .

# Response includes the new record ID
# {"id": "003xx000004TmiQAAS", "success": true, "errors": []}

Log an email as a Task

# Log an email as a completed Task linked to the Contact
EMAIL=$(cat emails.json | jq '.[0]')
CONTACT_ID="003xx000004TmiQAAS"  # from the Contact creation response

curl -s -X POST "$SF_INSTANCE/services/data/v59.0/sobjects/Task" \
  -H "Authorization: Bearer $SF_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(echo $EMAIL | jq --arg who "$CONTACT_ID" '{
    WhoId: $who,
    Subject: .subject,
    ActivityDate: (.date | split("T")[0]),
    Status: "Completed",
    Priority: "Normal",
    Type: "Email",
    Description: (.body[:500] // "")
  }')" | jq .

Create an Account from domain

# Create an Account from a sender domain
DOMAIN="acme.com"

curl -s -X POST "$SF_INSTANCE/services/data/v59.0/sobjects/Account" \
  -H "Authorization: Bearer $SF_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    "Name": "$(echo $DOMAIN | sed 's/\.com$//' | sed 's/^./\U&/')",
    "Website": "https://$DOMAIN"
  }" | jq .

Upsert to avoid duplicates

# Upsert a Contact by Email (requires Email as an External ID or unique field)
curl -s -X PATCH "$SF_INSTANCE/services/data/v59.0/sobjects/Contact/Email/jane@acme.com" \
  -H "Authorization: Bearer $SF_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "FirstName": "Jane",
    "LastName": "Smith",
    "Phone": "+1-555-0123",
    "Title": "VP Engineering"
  }' | jq .

Scheduled sync script

Automate the export-import cycle with a shell script and cron. This script exports emails received since the last run, transforms them into Salesforce API payloads, and upserts Contacts and Tasks.

#!/usr/bin/env bash
# sync-to-salesforce.sh — Export new emails from Nylas CLI and upsert into Salesforce
set -euo pipefail

STATE_FILE="$HOME/.nylas-sf-sync-state"
LOG_FILE="$HOME/.nylas-sf-sync.log"

# Load last sync timestamp (or default to 7 days ago)
if [[ -f "$STATE_FILE" ]]; then
  LAST_SYNC=$(cat "$STATE_FILE")
else
  LAST_SYNC=$(date -u -d '7 days ago' '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || \
              date -u -v-7d '+%Y-%m-%dT%H:%M:%S')
fi

echo "[$(date -u '+%Y-%m-%d %H:%M:%S')] Starting sync since $LAST_SYNC" >> "$LOG_FILE"

# Export emails since last sync
nylas email list --json --limit 200 > /tmp/sf-emails.json

# Filter to only emails after LAST_SYNC
cat /tmp/sf-emails.json | jq --arg since "$LAST_SYNC" '
  [.[] | select(.date > $since)]
' > /tmp/sf-new-emails.json

NEW_COUNT=$(cat /tmp/sf-new-emails.json | jq 'length')
echo "[$(date -u '+%Y-%m-%d %H:%M:%S')] Found $NEW_COUNT new emails" >> "$LOG_FILE"

if [[ "$NEW_COUNT" -eq 0 ]]; then
  echo "No new emails since $LAST_SYNC"
  exit 0
fi

# Authenticate to Salesforce
SF_RESPONSE=$(curl -s https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=password" \
  -d "client_id=$SF_CLIENT_ID" \
  -d "client_secret=$SF_CLIENT_SECRET" \
  -d "username=$SF_USERNAME" \
  -d "password=$SF_PASSWORD$SF_SECURITY_TOKEN")

SF_TOKEN=$(echo "$SF_RESPONSE" | jq -r '.access_token')
SF_INSTANCE=$(echo "$SF_RESPONSE" | jq -r '.instance_url')

if [[ "$SF_TOKEN" == "null" || -z "$SF_TOKEN" ]]; then
  echo "ERROR: Salesforce authentication failed" >> "$LOG_FILE"
  exit 1
fi

# Process each unique sender — upsert as Contact
cat /tmp/sf-new-emails.json | jq -c '
  [.[] | .from[0] | select(.email != null)]
  | group_by(.email)
  | map(.[0])
  | .[]
' | while read -r sender; do
  EMAIL=$(echo "$sender" | jq -r '.email')
  NAME=$(echo "$sender" | jq -r '.name // ""')
  FIRST=$(echo "$NAME" | awk '{print $1}')
  LAST=$(echo "$NAME" | awk '{$1=""; print substr($0,2)}')

  # Skip if no email
  [[ -z "$EMAIL" ]] && continue

  # Upsert Contact by email
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
    -X PATCH "$SF_INSTANCE/services/data/v59.0/sobjects/Contact/Email/$EMAIL" \
    -H "Authorization: Bearer $SF_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg fn "$FIRST" --arg ln "$LAST" '{
      FirstName: $fn,
      LastName: (if $ln == "" then "Unknown" else $ln end)
    }')")

  echo "  Contact $EMAIL -> HTTP $HTTP_CODE" >> "$LOG_FILE"
done

# Log emails as Tasks
cat /tmp/sf-new-emails.json | jq -c '.[]' | while read -r email_obj; do
  SUBJECT=$(echo "$email_obj" | jq -r '.subject // "No subject"')
  DATE=$(echo "$email_obj" | jq -r '.date | split("T")[0]')

  curl -s -o /dev/null \
    -X POST "$SF_INSTANCE/services/data/v59.0/sobjects/Task" \
    -H "Authorization: Bearer $SF_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$(echo "$email_obj" | jq '{
      Subject: .subject,
      ActivityDate: (.date | split("T")[0]),
      Status: "Completed",
      Priority: "Normal",
      Type: "Email",
      Description: (.body[:500] // "")
    }')"
done

# Save current timestamp for next run
date -u '+%Y-%m-%dT%H:%M:%S' > "$STATE_FILE"

echo "[$(date -u '+%Y-%m-%d %H:%M:%S')] Sync complete: $NEW_COUNT emails processed" >> "$LOG_FILE"
echo "Synced $NEW_COUNT emails to Salesforce"

Schedule it with cron to run every hour:

# Run every hour — edit with: crontab -e
0 * * * * /path/to/sync-to-salesforce.sh >> /tmp/sf-sync-cron.log 2>&1

Python version with simple_salesforce

For a more robust implementation, use Python with the simple_salesforce library. This script exports from Nylas CLI, creates or updates Contacts, and logs email as Tasks.

#!/usr/bin/env python3
"""Export email data from Nylas CLI and sync to Salesforce."""

import json
import os
import subprocess
import sys
from datetime import datetime

from simple_salesforce import Salesforce

# --- Salesforce connection ---

sf = Salesforce(
    username=os.environ["SF_USERNAME"],
    password=os.environ["SF_PASSWORD"],
    security_token=os.environ["SF_SECURITY_TOKEN"],
    domain="login",  # use "test" for sandbox
)

print(f"Connected to Salesforce: {sf.sf_instance}")


# --- Export from Nylas CLI ---

def fetch_emails(limit: int = 200) -> list[dict]:
    result = subprocess.run(
        ["nylas", "email", "list", "--json", "--limit", str(limit)],
        capture_output=True, text=True, check=True,
    )
    return json.loads(result.stdout)


def fetch_contacts(limit: int = 200) -> list[dict]:
    result = subprocess.run(
        ["nylas", "contacts", "list", "--json", "--limit", str(limit)],
        capture_output=True, text=True, check=True,
    )
    return json.loads(result.stdout)


# --- Upsert Contacts ---

def upsert_contact(contact: dict) -> str | None:
    """Create or update a Salesforce Contact. Returns the Contact ID."""
    emails = contact.get("emails", [])
    if not emails:
        return None

    email = emails[0].get("email", "")
    if not email:
        return None

    first_name = contact.get("given_name", "")
    last_name = contact.get("surname", "") or "Unknown"
    phone = ""
    phone_numbers = contact.get("phone_numbers", [])
    if phone_numbers:
        phone = phone_numbers[0].get("number", "")
    title = contact.get("job_title", "")
    company = contact.get("company_name", "")

    payload = {
        "FirstName": first_name,
        "LastName": last_name,
        "Email": email,
        "Phone": phone,
        "Title": title,
    }

    # Remove empty fields
    payload = {k: v for k, v in payload.items() if v}
    payload["LastName"] = last_name  # Required field

    try:
        # Try to find existing Contact by email
        results = sf.query(
            f"SELECT Id FROM Contact WHERE Email = '{email}' LIMIT 1"
        )
        if results["totalSize"] > 0:
            contact_id = results["records"][0]["Id"]
            sf.Contact.update(contact_id, payload)
            print(f"  Updated Contact: {email} ({contact_id})")
            return contact_id
        else:
            result = sf.Contact.create(payload)
            print(f"  Created Contact: {email} ({result['id']})")
            return result["id"]
    except Exception as e:
        print(f"  Error upserting {email}: {e}")
        return None


def upsert_contact_from_email(sender: dict) -> str | None:
    """Create or update a Contact from email sender data."""
    email = sender.get("email", "")
    name = sender.get("name", "")
    if not email:
        return None

    parts = name.split(" ", 1) if name else ["", ""]
    first_name = parts[0]
    last_name = parts[1] if len(parts) > 1 else "Unknown"

    payload = {
        "FirstName": first_name,
        "LastName": last_name,
        "Email": email,
    }

    try:
        results = sf.query(
            f"SELECT Id FROM Contact WHERE Email = '{email}' LIMIT 1"
        )
        if results["totalSize"] > 0:
            contact_id = results["records"][0]["Id"]
            sf.Contact.update(contact_id, payload)
            return contact_id
        else:
            result = sf.Contact.create(payload)
            return result["id"]
    except Exception as e:
        print(f"  Error upserting sender {email}: {e}")
        return None


# --- Log email as Task ---

def log_email_as_task(email_msg: dict, contact_id: str | None) -> None:
    """Create a Task record for an email."""
    subject = email_msg.get("subject", "No subject")
    date_str = email_msg.get("date", "")
    body = (email_msg.get("body", "") or "")[:500]

    # Parse date to YYYY-MM-DD
    activity_date = date_str.split("T")[0] if "T" in date_str else date_str

    payload = {
        "Subject": subject[:255],
        "ActivityDate": activity_date,
        "Status": "Completed",
        "Priority": "Normal",
        "Type": "Email",
        "Description": body,
    }

    if contact_id:
        payload["WhoId"] = contact_id

    try:
        result = sf.Task.create(payload)
        print(f"  Logged Task: {subject[:50]}... ({result['id']})")
    except Exception as e:
        print(f"  Error logging task: {e}")


# --- Main ---

def main():
    limit = int(sys.argv[1]) if len(sys.argv) > 1 else 200

    # Step 1: Export and upsert contacts
    print("Exporting contacts from Nylas CLI...")
    contacts = fetch_contacts(limit)
    print(f"Found {len(contacts)} contacts")

    contact_email_to_id: dict[str, str] = {}
    for contact in contacts:
        emails = contact.get("emails", [])
        if emails:
            email = emails[0].get("email", "")
            contact_id = upsert_contact(contact)
            if contact_id and email:
                contact_email_to_id[email] = contact_id

    print(f"
Upserted {len(contact_email_to_id)} contacts to Salesforce")

    # Step 2: Export emails and log as Tasks
    print("
Exporting emails from Nylas CLI...")
    emails = fetch_emails(limit)
    print(f"Found {len(emails)} emails")

    task_count = 0
    for email_msg in emails:
        sender = (email_msg.get("from") or [{}])[0]
        sender_email = sender.get("email", "")

        # Find or create the Contact for this sender
        contact_id = contact_email_to_id.get(sender_email)
        if not contact_id and sender_email:
            contact_id = upsert_contact_from_email(sender)
            if contact_id:
                contact_email_to_id[sender_email] = contact_id

        log_email_as_task(email_msg, contact_id)
        task_count += 1

    print(f"
Done: {len(contact_email_to_id)} contacts, {task_count} tasks synced to Salesforce")


if __name__ == "__main__":
    main()

Install the dependency and run:

pip install simple-salesforce

# Set credentials as environment variables
export SF_USERNAME="you@company.com"
export SF_PASSWORD="your-password"
export SF_SECURITY_TOKEN="your-security-token"

# Run the sync (default: 200 emails and contacts)
python3 sync_salesforce.py

# Or specify a higher limit
python3 sync_salesforce.py 500

TypeScript version with jsforce

For TypeScript/Node environments, use jsforce — the most popular Salesforce client for JavaScript.

import { execFileSync } from "child_process";
import jsforce from "jsforce";

// --- Types ---

interface NylasContact {
  given_name?: string;
  surname?: string;
  emails?: { email: string; type?: string }[];
  phone_numbers?: { number: string; type?: string }[];
  job_title?: string;
  company_name?: string;
}

interface NylasEmail {
  id: string;
  from: { email: string; name: string }[];
  to: { email: string; name: string }[];
  subject: string;
  date: string;
  body?: string;
}

// --- Salesforce connection ---

const conn = new jsforce.Connection({
  loginUrl: process.env.SF_LOGIN_URL || "https://login.salesforce.com",
});

async function authenticate(): Promise<void> {
  const username = process.env.SF_USERNAME!;
  const password = process.env.SF_PASSWORD! + (process.env.SF_SECURITY_TOKEN || "");
  await conn.login(username, password);
  console.log(`Connected to Salesforce: ${conn.instanceUrl}`);
}

// --- Export from Nylas CLI ---

function fetchEmails(limit = 200): NylasEmail[] {
  const output = execFileSync("nylas", ["email", "list", "--json", "--limit", String(limit)], {
    encoding: "utf-8",
    maxBuffer: 50 * 1024 * 1024,
  });
  return JSON.parse(output);
}

function fetchContacts(limit = 200): NylasContact[] {
  const output = execFileSync("nylas", ["contacts", "list", "--json", "--limit", String(limit)], {
    encoding: "utf-8",
    maxBuffer: 50 * 1024 * 1024,
  });
  return JSON.parse(output);
}

// --- Upsert Contacts ---

async function upsertContact(contact: NylasContact): Promise<string | null> {
  const email = contact.emails?.[0]?.email;
  if (!email) return null;

  const payload = {
    FirstName: contact.given_name || "",
    LastName: contact.surname || "Unknown",
    Email: email,
    Phone: contact.phone_numbers?.[0]?.number || "",
    Title: contact.job_title || "",
  };

  try {
    // Search for existing Contact
    const result = await conn.query<{ Id: string }>(
      `SELECT Id FROM Contact WHERE Email = '${email}' LIMIT 1`
    );

    if (result.totalSize > 0) {
      const contactId = result.records[0].Id;
      await conn.sobject("Contact").update({ Id: contactId, ...payload });
      console.log(`  Updated Contact: ${email} (${contactId})`);
      return contactId;
    } else {
      const created = await conn.sobject("Contact").create(payload);
      if (created.success) {
        console.log(`  Created Contact: ${email} (${created.id})`);
        return created.id;
      }
      return null;
    }
  } catch (err) {
    console.error(`  Error upserting ${email}: ${err}`);
    return null;
  }
}

async function upsertContactFromSender(
  sender: { email: string; name: string }
): Promise<string | null> {
  if (!sender.email) return null;

  const parts = (sender.name || "").split(" ");
  const firstName = parts[0] || "";
  const lastName = parts.slice(1).join(" ") || "Unknown";

  const payload = {
    FirstName: firstName,
    LastName: lastName,
    Email: sender.email,
  };

  try {
    const result = await conn.query<{ Id: string }>(
      `SELECT Id FROM Contact WHERE Email = '${sender.email}' LIMIT 1`
    );

    if (result.totalSize > 0) {
      const contactId = result.records[0].Id;
      await conn.sobject("Contact").update({ Id: contactId, ...payload });
      return contactId;
    } else {
      const created = await conn.sobject("Contact").create(payload);
      return created.success ? created.id : null;
    }
  } catch (err) {
    console.error(`  Error upserting sender ${sender.email}: ${err}`);
    return null;
  }
}

// --- Log email as Task ---

async function logEmailAsTask(
  email: NylasEmail,
  contactId: string | null
): Promise<void> {
  const activityDate = email.date?.split("T")[0] || "";
  const description = (email.body || "").slice(0, 500);

  const payload: Record<string, string> = {
    Subject: (email.subject || "No subject").slice(0, 255),
    ActivityDate: activityDate,
    Status: "Completed",
    Priority: "Normal",
    Type: "Email",
    Description: description,
  };

  if (contactId) {
    payload.WhoId = contactId;
  }

  try {
    const result = await conn.sobject("Task").create(payload);
    if (result.success) {
      console.log(`  Logged Task: ${email.subject?.slice(0, 50)}... (${result.id})`);
    }
  } catch (err) {
    console.error(`  Error logging task: ${err}`);
  }
}

// --- Main ---

async function main(): Promise<void> {
  const limit = Number(process.argv[2]) || 200;

  await authenticate();

  // Step 1: Export and upsert contacts
  console.log("Exporting contacts from Nylas CLI...");
  const contacts = fetchContacts(limit);
  console.log(`Found ${contacts.length} contacts`);

  const emailToContactId = new Map<string, string>();

  for (const contact of contacts) {
    const email = contact.emails?.[0]?.email;
    if (!email) continue;
    const contactId = await upsertContact(contact);
    if (contactId) {
      emailToContactId.set(email, contactId);
    }
  }

  console.log(`\nUpserted ${emailToContactId.size} contacts to Salesforce`);

  // Step 2: Export emails and log as Tasks
  console.log("\nExporting emails from Nylas CLI...");
  const emails = fetchEmails(limit);
  console.log(`Found ${emails.length} emails`);

  let taskCount = 0;
  for (const email of emails) {
    const sender = email.from?.[0];
    if (!sender?.email) continue;

    let contactId = emailToContactId.get(sender.email) ?? null;
    if (!contactId) {
      contactId = await upsertContactFromSender(sender);
      if (contactId) {
        emailToContactId.set(sender.email, contactId);
      }
    }

    await logEmailAsTask(email, contactId);
    taskCount++;
  }

  console.log(
    `\nDone: ${emailToContactId.size} contacts, ${taskCount} tasks synced to Salesforce`
  );
}

main().catch(console.error);

Install dependencies and run:

npm install jsforce
npm install -D @types/jsforce tsx

# Set credentials
export SF_USERNAME="you@company.com"
export SF_PASSWORD="your-password"
export SF_SECURITY_TOKEN="your-security-token"

# Run the sync
npx tsx sync_salesforce.ts

# Or with a custom limit
npx tsx sync_salesforce.ts 500

Next steps