Back to our work

Case study

FCC Email Scraper

Turning LinkedIn ad replies into a recruiting pipeline.

The Franchise Consulting Company recruits its franchise consultants from experienced professionals. They ran LinkedIn job ads to find them, and the applications poured in faster than anyone could read them. Cedar Web Services built the system that reads every reply and résumé, pulls out who the person is and how to reach them, and keeps the recruiting team’s call list current without a keystroke.

applications parsed
125,000+
fields extracted per applicant
7
sources read: the email and the résumé
2
hours of manual data entry
0
Illustration of a robot reading a stack of résumés and filing name, email, phone, and current role into a database

The brief

Find the people, then find their details.

01

The ads

The brokerage ran LinkedIn job ads written to attract people whose careers made them natural franchise consultants: sales leaders, business development managers, franchise operators, and executives looking for a second act. Every application arrived as an email in a dedicated jobs mailbox with the applicant’s résumé attached.

02

The problem

Replies came in by the hundreds every month. Each one had to be opened, the résumé downloaded, and the name, contact details, current role, and location typed into a spreadsheet before anyone on the recruiting team could pick up the phone. It was slow, it fell behind, and the best candidates were the ones who got called last.

03

The system

A Python service reads the mailbox, parses each application email and its résumé, extracts seven fields, and writes a structured row to the brokerage’s database. A second job pushes new rows to the top of the recruiting team’s Google Sheet so the newest applicants are always at row two. The team opens the sheet and starts calling.

How it flows

From an ad to a phone call.

Nothing in the middle needs a person. The recruiting team’s only job is the part that actually needs them: talking to candidates.

Pipeline: LinkedIn ad, applicant replies, jobs mailbox, the parser, database, then a Google Sheet and the admin lead log, ending with the recruiting team LinkedIn job ad targeted by career Application email HTML body + résumé Jobs mailbox read over IMAP Parser HTML → 5 fields résumé → phone, email regex, then Gemini Database one row each Google Sheet newest on top Lead log charts + export Recruiting team calls Fully automated. Runs on a schedule, unattended.

What gets extracted

Seven fields, two sources.

LinkedIn’s notification email says who applied, what they do now, where they are, and which ad they answered. It does not include a way to contact them. That lives in the résumé, so both are read and merged into a single record.

Field Read from
Applicant name Email
Current position Email
Current location Email
Position applied for Email
Location applied for Email
Email address Résumé
Phone number Résumé
Extraction: the email HTML yields five fields directly; the résumé is converted to text, matched with regular expressions, and sent to Gemini only if a phone or email is still missing; the results merge into one applicant record Email HTML LinkedIn’s fixed layout Résumé PDF or Word → text Parse markup name, roles, locations Regex match phone + email patterns Both found? Ask Gemini only for what is missing Applicant record 7 fields, one row complete or partial no yes

How it was built

Small program, careful edges.

The whole pipeline is a few hundred lines of Python. What made it run unattended for years is the handling around the edges: quotas, retries, duplicates, and crashes.

PythonIMAPBeautifulSoupPyPDF2python-docxGoogle GeminiMySQLGoogle Sheets APIThreadingcron

01

Two sources, one record

LinkedIn’s application email carries the applicant’s name, current position, location, and the role and location they applied for, in a fixed HTML layout. The parser reads those straight out of the markup. The email address and phone number live in the résumé, so the attachment is opened, whether PDF or Word, and its text is scanned.

02

Regex first, AI when it misses

Phone numbers and email addresses are matched with regular expressions first, because that is fast, free, and right most of the time. Only when a résumé defeats the patterns does the text go to Google Gemini with a single question: find the phone and email. The AI is a fallback, not the front door, which keeps cost and latency low.

03

Never the same applicant twice

Every run looks at a rolling window of recent mail. Before fetching anything heavy, it pulls only the message headers in large batches and checks the database for each message ID. Applications already stored with every field filled are skipped. Ones stored with gaps are marked partial and retried for just the missing fields. New ones are queued. A crash mid-run costs nothing and a re-run never creates a duplicate.

04

Fast without tripping Gmail’s quota

Gmail meters how much mail a client can download. Four worker threads parse résumés and call the AI in parallel, but full message downloads pass through one global rate limiter that hands out evenly spaced time slots across every thread. Overlapping the slow work is what makes it fast. Fetching faster is what would get it blocked, so it does not. If Gmail pushes back anyway, the fetch backs off and retries.

05

A spreadsheet the team already used

The recruiting team did not want a new tool. They wanted their sheet, newest at the top. The sync job reads the highest synced ID from a watermark cell in the sheet itself, selects everything newer from the database, and inserts the batch above row two in one API call, so the watermark advances on its own and the order is gap-free even if a run is missed.

06

Charted for the business

The same table feeds the lead log in the brokerage’s admin console, with daily, weekly, and monthly charts of applicant volume and CSV and Excel export. Ad spend on LinkedIn could be read against applications the next morning.

Every run

Skip, retry, or fetch.

Cheap header fetches first, one database lookup for the whole batch, and only then the expensive downloads, for just the messages that need them. This is why the job can be re-run at any time, and why a crash halfway through never leaves a mess.

Each run: list recent messages by header, look them all up in the database in one batch, then skip complete ones, retry partial ones for missing fields, and fully process new ones through the rate-limited worker pool Recent mail headers only, batched Database check one query per batch Complete → skip nothing downloaded Partial → retry fill only the missing fields New → process 4 workers, one shared rate limit

Got an inbox somebody has to read by hand?

Applications, orders, invoices, support requests. If a person is copying details out of emails into a spreadsheet, we can build the thing that does it for them, and does it right when the volume spikes.

Start a conversation