Many property portals still deliver leads to small agencies as notification emails. Before you add an AI assistant to the follow-up, turn those emails into clean, structured records. Clean input makes everything downstream simpler, from dedupe to follow-up timers.
This intake step is the unglamorous start of real estate lead automation for small agencies, and it is worth getting right.
{"source": "portal_a", "received_at": "2026-10-19T09:12:00+05:30",
"name": "...", "phone": "...", "email": "...",
"listing_ref": "...", "message": "...", "raw_id": "<message-id>"}
Each portal formats its emails differently, so write one small parser per sender address and route by the From header.
import re
def parse_portal_a(body: str) -> dict:
def grab(pattern):
m = re.search(pattern, body)
return m.group(1).strip() if m else None
return {
"name": grab(r"Name:\s*(.+)"),
"phone": grab(r"Phone:\s*([+\d][\d\s-]+)"),
"email": grab(r"Email:\s*(\S+@\S+)"),
"listing_ref": grab(r"Ref(?:erence)?:\s*(\S+)"),
"message": grab(r"Message:\s*([\s\S]+?)\n\s*\n"),
}
PARSERS = {"leads@portal-a.example": parse_portal_a}
def parse(email):
parser = PARSERS.get(email.sender)
if parser is None:
return route_to_human(email, reason="unknown_sender")
record = parser(email.text_body)
record.update(source=email.sender, raw_id=email.message_id,
received_at=email.date)
return record
python
REQUIRED = ("listing_ref",)
ONE_OF = ("phone", "email")
def validate(r):
problems = [f for f in REQUIRED if not r.get(f)]
if not any(r.get(f) for f in ONE_OF):
problems.append("no_contact")
return problems
If validate returns anything, send the email to a person with the reason attached. Do not guess.
None for the phone number creates leads nobody can call.
Dedupe and follow-up timers come after this step. They are much easier when every lead arrives in the same shape.
Disclosure: I run NxFlowAI, an AI automation agency. Mapping intake like this is part of our first audit, which takes 72 hours for one workflow.