Import Email Signatures Into Your CRM With an Agent A developer built a system to extract structured data from email signatures using regex and cross-referencing, achieving 91% field completeness by merging data from three messages. The system uses a dedicated Nylas agent account with a webhook to passively enrich CRM data or allow user-initiated imports. The approach avoids costly LLM calls by relying on regex for 95% of signatures, with LLM fallback only for edge cases. Email signatures are the most valuable dataset your CRM is throwing away. Roughly 82% of business email carries a signature with at least a name and title — and usually a phone number, a LinkedIn URL, a company name, sometimes a whole org-chart hint. That's structured data masquerading as prose, delivered free with every message, and most platforms scroll right past it. You don't need a data vendor or even an LLM to harvest it. A few hundred lines of regex, a cross-referencing trick, and a dedicated inbox for the agent doing the work gets you to production-usable accuracy. Here's the build. For genuinely unstructured prose, a model wins. Signatures aren't unstructured — they're predictably structured: 3–6 lines, often separated from the body by the RFC 3676 -- delimiter, drawing from a small set of field types. A regex pass catches over 95% of well-formed signatures, runs in microseconds, and costs nothing per message. Keep the LLM as a fallback for the weird 5%, and skip it entirely in version one. Find the boundary first: python import re SIG DELIMITERS = r"\n--\s \n", RFC 3676 standard r"\nSent from my iPhone|iPad|Android ", r"\nBest,?\s \n", r"\nRegards,?\s \n", r"\nCheers,?\s \n", def split signature body: str - tuple str, str : for pat in SIG DELIMITERS: m = re.search pat, body if m: return body :m.start , body m.end : return body, "" Then pull fields — phone, LinkedIn /in/ only; the /pub/ URL shape was retired years ago , website, plus title and company against a keyword vocabulary. The detail that makes this sales-relevant: classify titles into tiers C-suite, VP, Director, Manager, IC . "CEO" as a routing signal is worth far more than the raw title string. One email rarely gives you a complete picture. The "Sent from my iPhone" reply has nothing. The quick thank-you has just a name. The mid-thread message has the full block. So don't extract from one message — pull the last three from the same sender, extract from each, and merge, taking the most complete value per field: php def enrich sender email: str, n: int = 3 - dict: messages = list messages from sender email, limit=n signatures = split signature m "body" 1 for m in messages fields = extract s for s in signatures return merge fields fields The lift is the headline number: single-message extraction nets about 67% field completeness; three-message cross-referencing hits about 91%. That's the difference between a column nobody trusts and one your sales team filters on. Bonus intelligence that costs three DNS queries: the sender's domain reveals their mail host via MX records, their tooling via SPF includes SendGrid, Salesforce... , and their security maturity via DMARC. Free enrichment, no email body required. Where does the mail come from? Two patterns, same infrastructure — a dedicated Agent Account https://developer.nylas.com/docs/v3/agent-accounts/ Nylas-hosted mailboxes, currently in beta with a message.created webhook. Pattern one: passive enrichment. The agent's own inbox — support@, outreach@ — already receives business mail. Every inbound message is a parsing opportunity; the webhook handler extracts, cross-references, and writes to the CRM. Pattern two: user-initiated import. Stand up signatureimport@agents.yourcompany.com and tell users: forward any email that has the signature you want. Your handler identifies the forwarder, extracts the signature HTML, and saves it to their grant: js app.post "/webhooks/signature-import", async req, res = { res.status 200 .end ; const event = req.body; if event.type == "message.created" return; const msg = event.data.object; if msg.grant id == IMPORT GRANT ID return; // The webhook payload is a summary — fetch the full body. const full = await nylas.messages.find { identifier: IMPORT GRANT ID, messageId: msg.id, } ; // Whoever forwarded this is the user whose grant gets the signature. const forwarder = msg.from 0 .email; const targetGrant = await db.grants.findByEmail forwarder ; if targetGrant return; // unknown address — log and ignore const signatureHtml = await extractSignature full.data.body ; if signatureHtml return; await saveSignature targetGrant.grantId, signatureHtml, forwarder ; } ; The mapping lookup is the load-bearing line: your app needs a table from user email address to grant id , and an unknown forwarder should be ignored, not guessed at. The save itself goes through the Signatures API: js const signature = await nylas.signatures.create { identifier: targetGrantId, requestBody: { name: "Imported signature", body: signatureHtml, }, } ; One import inbox serves every user — route by the from address on the forwarded email. The saved signature then attaches to outbound sends with a signature id parameter, which works on agent mailboxes too: an outreach agent can send with a real, human-looking signature imported this way. Forwarded message boundary.