Every message that lands in a connected mailbox carries structured contact data: a sender, a list of recipients, and often a reply-to address. Most teams let that data rot in the inbox and ask users to type contacts in by hand. You can do better. Pull the participants off each message, normalize the addresses, and write them straight into your contact database.
This guide shows how to parse sender and recipient email addresses from messages with the Nylas Email API, then upsert each one as a contact so your CRM stays current without manual entry.
How do I parse email addresses in Python to enrich a contact database?
Section titled “How do I parse email addresses in Python to enrich a contact database?”Read messages with GET /v3/grants/{grant_id}/messages, then pull the from, to, cc, and bcc arrays off each message. Every entry is a {name, email} object, so no regex parsing is needed. Lowercase each address, dedupe, and write the result to POST /v3/grants/{grant_id}/contacts. One list call returns up to 50 messages per page.
The request below lists the most recent messages for a connected grant. Use the limit parameter to control page size, which defaults to 50 and caps at 200 per request. The response gives you parsed participant objects on every message, which is the data you’ll turn into contacts.
curl --request GET \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?limit=50' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'from nylas import Client
nylas = Client(api_key="<NYLAS_API_KEY>")
messages = nylas.messages.list( "<NYLAS_GRANT_ID>", query_params={"limit": 50},)
# Collect every address across from / to / cc / bccseen = {}for message in messages.data: participants = ( (message.from_ or []) + (message.to or []) + (message.cc or []) + (message.bcc or []) ) for person in participants: address = (person.get("email") or "").strip().lower() if address: seen[address] = person.get("name") or ""
print(f"Found {len(seen)} unique addresses")The from_ attribute is spelled with a trailing underscore in Python because from is a reserved keyword. Each provider returns the same participant shape, so this loop works on Google, Microsoft, Yahoo, iCloud, IMAP, and Exchange accounts without per-provider branching.
How do I pull sender and recipient addresses from a message?
Section titled “How do I pull sender and recipient addresses from a message?”To extract contacts from a thread, filter the message list to one conversation and read the participants off each message. Pass a thread_id to GET /v3/grants/{grant_id}/messages, or use any_email to find every message touching a given address. The any_email parameter accepts up to 25 comma-separated addresses per request, which covers most threads in one call.
The request below pulls every message tied to a thread, so you capture participants who joined partway through. Threads accumulate addresses as people are added to the conversation, so reading the whole thread catches recipients a single message would miss. Each match still returns the same {name, email} participant objects.
curl --request GET \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?thread_id=<THREAD_ID>&limit=50' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'thread_messages = nylas.messages.list( "<NYLAS_GRANT_ID>", query_params={"thread_id": "<THREAD_ID>", "limit": 50},)
contacts = {}for message in thread_messages.data: for person in (message.to or []) + (message.cc or []): address = (person.get("email") or "").strip().lower() if address and address not in contacts: contacts[address] = person.get("name") or ""One practical filter: skip your own grant’s address and known internal domains before you upsert. Otherwise you’ll create a contact record for the mailbox owner and every teammate on the thread. A small allowlist of domains to ignore keeps the database focused on external people, which is what a CRM actually wants.
If you also want names and titles from signature blocks rather than just the header participants, that’s a separate parsing job on the message body. The extract contacts from email threads guide covers signature parsing, which complements the participant approach here. Header parsing gives you a clean address on all 50 messages a page returns; signature parsing only helps on the subset of business messages that actually carry a signature block.
How do I manage email contacts programmatically for a CRM SaaS product?
Section titled “How do I manage email contacts programmatically for a CRM SaaS product?”Write each parsed address to POST /v3/grants/{grant_id}/contacts. The endpoint takes an emails array of {email, type} objects plus optional fields like given_name, surname, and company_name. There’s no native “upsert” verb, so check for an existing contact with the email filter on the list endpoint first, then create only when nothing matches.
The call below creates a contact from a parsed address. Set source to address_book so the record lives in the user’s editable contacts rather than the read-only inbox source the API derives automatically. Splitting the display name into given_name and surname keeps records consistent across the providers you sync.
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/contacts' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "given_name": "Leyah", "surname": "Miller", "company_name": "Acme Robotics", "source": "address_book", "emails": [{ "email": "[email protected]", "type": "work" }] }'def upsert_contact(address, name): existing = nylas.contacts.list( "<NYLAS_GRANT_ID>", query_params={"email": address, "limit": 1}, ) if existing.data: return existing.data[0]
given, _, surname = name.partition(" ") return nylas.contacts.create( "<NYLAS_GRANT_ID>", request_body={ "given_name": given or address.split("@")[0], "surname": surname, "source": "address_book", "emails": [{"email": address, "type": "work"}], }, ).data
for address, name in seen.items(): upsert_contact(address, name)For a multi-tenant SaaS product, run this loop per grant on a schedule and store a watermark so you only process new messages. The contacts endpoint returns 429 when you exceed the per-account rate limit, so batch your writes and back off on that status. The list-then-create check above prevents duplicate records, which is the failure mode that wrecks a contact database first.
Nylas vs parsing raw email headers yourself
Section titled “Nylas vs parsing raw email headers yourself”You could skip the API and parse From and To headers out of raw MIME with Python’s email.utils.parseaddr. That works, but you own the OAuth flow, token refresh, IMAP and Graph differences, and the edge cases of malformed headers and encoded display names. The table compares the two paths on the work contact enrichment actually requires.
| Task | Raw header parsing | Nylas Email API |
|---|---|---|
| Auth | Per-provider OAuth, IMAP, and Graph yourself | 1 grant, OAuth handled for you |
| Address parsing | parseaddr plus MIME decoding edge cases | Parsed {name, email} objects |
| Provider coverage | Build Gmail, Graph, and IMAP separately | Google, Microsoft, Yahoo, iCloud, IMAP, Exchange |
| Write contacts | None, build your own store | POST /v3/grants/{grant_id}/contacts |
| Thread context | Reassemble threads by header | thread_id and any_email filters |
Raw parsing is the right call when you already run your own mail server and never touch a hosted provider. The moment you add Gmail or Microsoft 365, the unified read path saves you two OAuth integrations and a pile of header-decoding code. Python’s email.utils module documents the parsing rules if you go that route.
There’s a real cost on the hosted side too. The list endpoint caps each page at 200 messages (50 by default), so backfilling a year of history means paging in a loop and respecting the 429 rate limit. If you only ever read a single mailbox you own, parsing raw MIME locally has no network round trips and no rate ceiling. The API earns its place once you cross more than one provider or want parsed participant objects without writing a decoder for RFC 2047 encoded display names.
What’s next
Section titled “What’s next”- List and sync contacts for the inbox-derived contact source and sync details
- Create and update contacts for the full contact field reference and update flow
- Recipient autocomplete to surface these enriched contacts in a compose UI
- Getting started with Nylas to create a project, connector, and your first grant