{"slug": "parsing-property-portal-lead-emails-into-clean-records-before-any-ai-touches", "title": "Parsing Property Portal Lead Emails Into Clean Records Before Any AI Touches Them", "summary": "The operator of NxFlowAI, an AI automation agency, outlined a per-sender email parsing approach for converting property portal lead notifications into a uniform record schema before any AI follow-up is added. The method routes messages by the From header to portal-specific regex parsers, then validates that a listing reference and at least one contact channel are present, sending failures to a human with a reason attached rather than guessing. The agency says this intake mapping is part of a first audit that takes 72 hours per workflow.", "body_md": "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.\n\nThis intake step is the unglamorous start of [real estate lead automation for small agencies](https://www.nxflowai.com/services), and it is worth getting right.\n\n```\n{\"source\": \"portal_a\", \"received_at\": \"2026-10-19T09:12:00+05:30\",\n \"name\": \"...\", \"phone\": \"...\", \"email\": \"...\",\n \"listing_ref\": \"...\", \"message\": \"...\", \"raw_id\": \"<message-id>\"}\n```\n\nEach portal formats its emails differently, so write one small parser per sender address and route by the `From` header.\n\n``` php\nimport re\n\ndef parse_portal_a(body: str) -> dict:\n    def grab(pattern):\n        m = re.search(pattern, body)\n        return m.group(1).strip() if m else None\n    return {\n        \"name\": grab(r\"Name:\\s*(.+)\"),\n        \"phone\": grab(r\"Phone:\\s*([+\\d][\\d\\s-]+)\"),\n        \"email\": grab(r\"Email:\\s*(\\S+@\\S+)\"),\n        \"listing_ref\": grab(r\"Ref(?:erence)?:\\s*(\\S+)\"),\n        \"message\": grab(r\"Message:\\s*([\\s\\S]+?)\\n\\s*\\n\"),\n    }\n\nPARSERS = {\"leads@portal-a.example\": parse_portal_a}\n\ndef parse(email):\n    parser = PARSERS.get(email.sender)\n    if parser is None:\n        return route_to_human(email, reason=\"unknown_sender\")\n    record = parser(email.text_body)\n    record.update(source=email.sender, raw_id=email.message_id,\n                  received_at=email.date)\n    return record\npython\nREQUIRED = (\"listing_ref\",)\nONE_OF = (\"phone\", \"email\")\n\ndef validate(r):\n    problems = [f for f in REQUIRED if not r.get(f)]\n    if not any(r.get(f) for f in ONE_OF):\n        problems.append(\"no_contact\")\n    return problems\n```\n\nIf `validate` returns anything, send the email to a person with the reason attached. Do not guess.\n\n`None` for the phone number creates leads nobody can call.\nDedupe and follow-up timers come after this step. They are much easier when every lead arrives in the same shape.\n\n*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.*", "url": "https://wpnews.pro/news/parsing-property-portal-lead-emails-into-clean-records-before-any-ai-touches", "canonical_source": "https://dev.to/ujjwal_dubey_9/parsing-property-portal-lead-emails-into-clean-records-before-any-ai-touches-them-473g", "published_at": "2026-10-19 05:06:29+00:00", "updated_at": "2026-10-19 05:18:07.737629+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": ["NxFlowAI"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/parsing-property-portal-lead-emails-into-clean-records-before-any-ai-touches", "markdown": "https://wpnews.pro/news/parsing-property-portal-lead-emails-into-clean-records-before-any-ai-touches.md", "text": "https://wpnews.pro/news/parsing-property-portal-lead-emails-into-clean-records-before-any-ai-touches.txt", "jsonld": "https://wpnews.pro/news/parsing-property-portal-lead-emails-into-clean-records-before-any-ai-touches.jsonld"}}