Guide
Enrich Contact and Company Info from Email
Email signatures are the most underused data source in sales and operations. Most people include their job title, phone number, company name, and often LinkedIn URL — and they update it every time they change roles. This guide shows you how to extract all of that programmatically.
Why email signatures are an enrichment goldmine
Every business email ends with a block of structured data that people maintain voluntarily. Job titles, direct phone numbers, company names, LinkedIn profiles, Twitter handles, office addresses — all sitting in plain text at the bottom of messages you already have. Unlike CRM records that go stale the moment someone changes roles, signatures are updated by the contacts themselves. When Sarah gets promoted from Director to VP, her signature changes before your CRM does.
The trick is that this data is unstructured enough to look messy but structured enough to parse reliably. Phone numbers follow predictable formats. LinkedIn URLs have a consistent pattern. Job titles cluster around a known vocabulary. And because signatures appear in every email, you can cross-reference across messages to fill gaps — if one email from Sarah has her phone number and another has her LinkedIn, you combine both into a single enriched record.
Parse email signatures
Start by pulling the full email body. The Nylas CLI returns the complete message content including the signature block. Signatures typically appear after a delimiter — a line of dashes, an underscore row, or a phrase like “Best regards” or “Sent from.”
# Get the full email body
nylas email read <message-id> --json | jq -r '.body'
# Signatures typically appear after a delimiter
# Common delimiters: "--", "___", "Best regards", "Sent from"
# Save the body to a variable for parsing
BODY=$(nylas email read <message-id> --json | jq -r '.body')Once you have the body text, use regex patterns to extract structured fields. These patterns handle the most common signature formats across industries.
# Extract phone numbers (handles international formats)
echo "$BODY" | grep -oE '\+?[0-9][\d\s\-().]{8,}[0-9]'
# Extract LinkedIn URLs
echo "$BODY" | grep -oE 'linkedin\.com/in/[a-zA-Z0-9_-]+'
# Extract likely job titles (lines containing common title keywords)
echo "$BODY" | grep -iE '(CEO|CTO|VP|Director|Manager|Engineer|Analyst|Lead|Head of|Founder|Partner|Consultant)'
# Extract Twitter/X handles
echo "$BODY" | grep -oE '@[a-zA-Z0-9_]{1,15}' | grep -v '@.*\.'
# Extract company website URLs
echo "$BODY" | grep -oE 'https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'Extract company info from DNS
The sender’s email domain tells you more than just the company name. DNS records reveal what tools a company uses, how large their infrastructure is, and what email provider they rely on. This is free intelligence that requires no API key.
# MX records reveal email provider
dig +short MX acme.com
# If result contains google.com → Google Workspace
# If result contains outlook.com → Microsoft 365
# If result contains mimecast.com → enterprise email security
# TXT/SPF records reveal infrastructure
dig +short TXT acme.com | grep "v=spf1"
# Mentions: include:_spf.google.com → uses Google
# Mentions: include:spf.protection.outlook.com → uses Microsoft
# Mentions: include:sendgrid.net → uses SendGrid for transactional email
# Mentions: include:mailchimp.com → uses Mailchimp for marketing
# DMARC records reveal security posture
dig +short TXT _dmarc.acme.com
# p=reject → strict email security (likely enterprise)
# p=none → lax or no DMARC enforcement (likely smaller org)MX records are the most informative. A company using Google Workspace is likely a tech company or startup. Microsoft 365 is common in enterprise and traditional industries. Self-hosted mail servers suggest either a large organization with dedicated IT or a security-conscious company. SPF records add another layer — the services listed in SPF tells you what marketing, transactional, and CRM tools the company uses.
Infer seniority from patterns
Job titles are the most obvious seniority signal, but email metadata contains several other patterns that correlate with seniority. Combining multiple signals produces a more accurate picture than any single data point.
Title keywords map to broad seniority tiers. C-suite titles (CEO, CTO, CFO, COO) and “Founder” indicate executive leadership. VP, SVP, and EVP indicate senior leadership. Director and “Head of” indicate mid-senior management. Manager, Lead, and Principal indicate mid-level. Engineer, Analyst, Associate, and Coordinator indicate individual contributors.
Email alias patterns correlate with seniority in surprising ways. First-name-only addresses like sarah@acme.com trend more senior — they joined early or had enough clout to claim the short alias. First-dot-last addresses like sarah.jones@acme.com are the standard pattern for larger organizations and later hires.
CC frequency is a strong signal. People who are frequently CC’d on threads but rarely initiate them are typically stakeholders or decision-makers — they’re kept in the loop because their approval matters.
Calendar patterns add another dimension. People who organize external meetings are client-facing and typically more senior. Recurring one-on-ones with multiple people suggest a management role.
# Seniority scoring example
# title_score: C-suite=5, VP=4, Director=3, Manager=2, IC=1
# alias_score: firstname@=2, first.last@=1, other=0
# cc_score: top 10% CC'd = 2, otherwise 0
# Total = title_score + alias_score + cc_score
# Example: Sarah Chen <sarah@acme.com>, VP of Engineering
# title_score = 4 (VP)
# alias_score = 2 (first-name-only)
# cc_score = 2 (frequently CC'd)
# Total = 8 → senior decision-makerBuild an enriched contact record
The goal is to assemble a single JSON record per contact that combines signature data, DNS intelligence, seniority scoring, and interaction history. This is the structure your CRM, sales tools, or agent workflows can consume.
{
"email": "sarah@acme.com",
"name": "Sarah Chen",
"company": "Acme Corp",
"domain": "acme.com",
"title": "VP of Engineering",
"phone": "+1-555-0142",
"linkedin": "linkedin.com/in/sarahchen",
"seniority_score": 8,
"email_provider": "Google Workspace",
"relationship_strength": 72,
"last_interaction": "2026-03-10"
}Here is a complete Bash pipeline that builds this record by combining email data, signature parsing, and DNS lookups.
#!/usr/bin/env bash
# Build an enriched contact record from email data
# Usage: ./enrich.sh <message-id>
MESSAGE_ID="$1"
if [ -z "$MESSAGE_ID" ]; then
echo "Usage: $0 <message-id>" >&2
exit 1
fi
# Pull the message
MSG=$(nylas email read "$MESSAGE_ID" --json)
BODY=$(echo "$MSG" | jq -r '.body')
FROM_EMAIL=$(echo "$MSG" | jq -r '.from[0].email')
FROM_NAME=$(echo "$MSG" | jq -r '.from[0].name')
DOMAIN=$(echo "$FROM_EMAIL" | cut -d@ -f2)
DATE=$(echo "$MSG" | jq -r '.date')
# Parse signature fields
PHONE=$(echo "$BODY" | grep -oE '\+?[0-9][0-9 \-().]{8,}[0-9]' | head -1)
LINKEDIN=$(echo "$BODY" | grep -oE 'linkedin\.com/in/[a-zA-Z0-9_-]+' | head -1)
TITLE=$(echo "$BODY" | grep -iE '(CEO|CTO|CFO|COO|VP|SVP|EVP|Director|Head of|Manager|Lead|Principal|Founder|Partner)' | head -1 | xargs)
# DNS lookup for email provider
MX=$(dig +short MX "$DOMAIN" | head -1)
if echo "$MX" | grep -qi 'google'; then
PROVIDER="Google Workspace"
elif echo "$MX" | grep -qi 'outlook\|microsoft'; then
PROVIDER="Microsoft 365"
else
PROVIDER="Other"
fi
# Seniority scoring
TITLE_SCORE=1
if echo "$TITLE" | grep -qiE '(CEO|CTO|CFO|COO|Founder)'; then TITLE_SCORE=5
elif echo "$TITLE" | grep -qiE '(VP|SVP|EVP)'; then TITLE_SCORE=4
elif echo "$TITLE" | grep -qiE '(Director|Head of)'; then TITLE_SCORE=3
elif echo "$TITLE" | grep -qiE '(Manager|Lead|Principal)'; then TITLE_SCORE=2
fi
ALIAS_SCORE=0
LOCAL=$(echo "$FROM_EMAIL" | cut -d@ -f1)
if ! echo "$LOCAL" | grep -q '\.'; then ALIAS_SCORE=2
else ALIAS_SCORE=1
fi
SENIORITY=$((TITLE_SCORE + ALIAS_SCORE))
# Output enriched record
jq -n \
--arg email "$FROM_EMAIL" \
--arg name "$FROM_NAME" \
--arg domain "$DOMAIN" \
--arg title "$TITLE" \
--arg phone "$PHONE" \
--arg linkedin "$LINKEDIN" \
--argjson seniority "$SENIORITY" \
--arg provider "$PROVIDER" \
--arg last_interaction "$DATE" \
'{
email: $email,
name: $name,
company: ($domain | split(".")[0] | (.[:1] | ascii_upcase) + .[1:]),
domain: $domain,
title: $title,
phone: $phone,
linkedin: $linkedin,
seniority_score: $seniority,
email_provider: $provider,
last_interaction: $last_interaction
}'Python version
A more robust approach using Python with regex patterns. The SignatureParser class extracts structured fields, and enrich_contact orchestrates the full pipeline.
#!/usr/bin/env python3
"""Enrich contacts from email signatures using Nylas CLI."""
import json
import re
import subprocess
class SignatureParser:
"""Extract structured fields from email signature text."""
PHONE_RE = re.compile(
r"\+?[\d][\d\s\-().]{8,}[\d]"
)
LINKEDIN_RE = re.compile(
r"linkedin\.com/in/[a-zA-Z0-9_-]+"
)
TITLE_KEYWORDS = [
"CEO", "CTO", "CFO", "COO", "Founder", "Partner",
"VP", "SVP", "EVP", "Vice President",
"Director", "Head of",
"Manager", "Lead", "Principal",
"Engineer", "Analyst", "Consultant", "Associate",
]
TITLE_RE = re.compile(
r"(?i)\b(" + "|".join(TITLE_KEYWORDS) + r")\b.*",
)
SENIORITY_MAP = {
5: ["CEO", "CTO", "CFO", "COO", "Founder"],
4: ["VP", "SVP", "EVP", "Vice President"],
3: ["Director", "Head of"],
2: ["Manager", "Lead", "Principal"],
1: ["Engineer", "Analyst", "Consultant", "Associate", "Partner"],
}
def extract_phone(self, text: str) -> str | None:
match = self.PHONE_RE.search(text)
return match.group(0).strip() if match else None
def extract_linkedin(self, text: str) -> str | None:
match = self.LINKEDIN_RE.search(text)
return match.group(0) if match else None
def extract_title(self, text: str) -> str | None:
for line in text.splitlines():
if self.TITLE_RE.search(line):
return line.strip()
return None
def score_seniority(self, title: str | None, email: str) -> int:
title_score = 0
if title:
for score, keywords in self.SENIORITY_MAP.items():
if any(k.lower() in title.lower() for k in keywords):
title_score = score
break
local = email.split("@")[0]
alias_score = 2 if "." not in local else 1
return title_score + alias_score
def run_cli(*args: str) -> str:
result = subprocess.run(
["nylas", *args],
capture_output=True, text=True, check=True,
)
return result.stdout
def enrich_contact(message_id: str) -> dict:
"""Build an enriched contact record from a single email."""
raw = run_cli("email", "read", message_id, "--json")
msg = json.loads(raw)
body = msg.get("body", "")
from_field = msg["from"][0]
email_addr = from_field["email"]
domain = email_addr.split("@")[1]
parser = SignatureParser()
# DNS lookup for email provider
try:
mx = subprocess.run(
["dig", "+short", "MX", domain],
capture_output=True, text=True, check=True,
).stdout.lower()
if "google" in mx:
provider = "Google Workspace"
elif "outlook" in mx or "microsoft" in mx:
provider = "Microsoft 365"
else:
provider = "Other"
except subprocess.CalledProcessError:
provider = "Unknown"
title = parser.extract_title(body)
return {
"email": email_addr,
"name": from_field.get("name", ""),
"domain": domain,
"title": title,
"phone": parser.extract_phone(body),
"linkedin": parser.extract_linkedin(body),
"seniority_score": parser.score_seniority(title, email_addr),
"email_provider": provider,
"last_interaction": msg.get("date", ""),
}
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python enrich.py <message-id>", file=sys.stderr)
sys.exit(1)
contact = enrich_contact(sys.argv[1])
print(json.dumps(contact, indent=2))TypeScript version
The same approach in TypeScript, with a typed ContactProfile interface and a SignatureParser class.
#!/usr/bin/env npx tsx
/**
* Enrich contacts from email signatures using Nylas CLI.
* Usage: npx tsx enrich.ts <message-id>
*/
import { execFileSync } from "child_process";
interface ContactProfile {
email: string;
name: string;
domain: string;
title: string | null;
phone: string | null;
linkedin: string | null;
seniorityScore: number;
emailProvider: string;
lastInteraction: string;
}
class SignatureParser {
private static PHONE_RE = /\+?[\d][\d\s\-().]{8,}[\d]/;
private static LINKEDIN_RE = /linkedin\.com\/in\/[a-zA-Z0-9_-]+/;
private static TITLE_KEYWORDS = [
"CEO", "CTO", "CFO", "COO", "Founder", "Partner",
"VP", "SVP", "EVP", "Vice President",
"Director", "Head of",
"Manager", "Lead", "Principal",
"Engineer", "Analyst", "Consultant", "Associate",
];
private static TITLE_RE = new RegExp(
"\\b(" + SignatureParser.TITLE_KEYWORDS.join("|") + ")\\b.*",
"i",
);
private static SENIORITY: Record<number, string[]> = {
5: ["CEO", "CTO", "CFO", "COO", "Founder"],
4: ["VP", "SVP", "EVP", "Vice President"],
3: ["Director", "Head of"],
2: ["Manager", "Lead", "Principal"],
1: ["Engineer", "Analyst", "Consultant", "Associate", "Partner"],
};
extractPhone(text: string): string | null {
const match = text.match(SignatureParser.PHONE_RE);
return match ? match[0].trim() : null;
}
extractLinkedin(text: string): string | null {
const match = text.match(SignatureParser.LINKEDIN_RE);
return match ? match[0] : null;
}
extractTitle(text: string): string | null {
for (const line of text.split("\n")) {
if (SignatureParser.TITLE_RE.test(line)) {
return line.trim();
}
}
return null;
}
scoreSeniority(title: string | null, email: string): number {
let titleScore = 0;
if (title) {
for (const [score, keywords] of Object.entries(SignatureParser.SENIORITY)) {
if (keywords.some((k) => title.toLowerCase().includes(k.toLowerCase()))) {
titleScore = Number(score);
break;
}
}
}
const local = email.split("@")[0];
const aliasScore = local.includes(".") ? 1 : 2;
return titleScore + aliasScore;
}
}
function runCli(...args: string[]): string {
return execFileSync("nylas", args, { encoding: "utf-8" });
}
function enrichContact(messageId: string): ContactProfile {
const raw = runCli("email", "read", messageId, "--json");
const msg = JSON.parse(raw);
const body: string = msg.body ?? "";
const fromField = msg.from[0];
const emailAddr: string = fromField.email;
const domain = emailAddr.split("@")[1];
const parser = new SignatureParser();
// DNS lookup for email provider
let provider = "Unknown";
try {
const mx = execFileSync("dig", ["+short", "MX", domain], {
encoding: "utf-8",
}).toLowerCase();
if (mx.includes("google")) provider = "Google Workspace";
else if (mx.includes("outlook") || mx.includes("microsoft"))
provider = "Microsoft 365";
else provider = "Other";
} catch {
// DNS lookup failed
}
const title = parser.extractTitle(body);
return {
email: emailAddr,
name: fromField.name ?? "",
domain,
title,
phone: parser.extractPhone(body),
linkedin: parser.extractLinkedin(body),
seniorityScore: parser.scoreSeniority(title, emailAddr),
emailProvider: provider,
lastInteraction: msg.date ?? "",
};
}
const messageId = process.argv[2];
if (!messageId) {
console.error("Usage: npx tsx enrich.ts <message-id>");
process.exit(1);
}
const contact = enrichContact(messageId);
console.log(JSON.stringify(contact, null, 2));Next steps
- Personalize Outbound Email from the CLI — use enriched contact data to send personalized emails at scale with merge templates and timezone-aware scheduling.
- Import Email into a Graph Database — model enriched contacts as graph nodes with relationship edges for network analysis, connector identification, and introduction path finding.
- CRM Email Workflows — the full series hub with all 8 guides from extraction to automated outbound.