Interview Email Parser
On-device regex + NSDataDetector engine that extracts date, time, time zone, meeting URL, and company name from interview invitation emails. Presents a confirmation sheet to match the parsed interview to a tracked job and add an EKEvent to the user's calendar.
Preview
Email / Share text
Parsed fields
high confidence
CompanyMeridian
Date & TimeThursday, June 12th at 2:00 PM
Time ZoneEST
PlatformZoom
Meeting URLzoom.us/j/…
Edit the email text and click Parse to see field extraction in real time
Signal extraction
The parser uses two different strategies for date/time vs. other fields.
| Field | Strategy | Fallback |
|---|---|---|
| Date & Time | NSDataDetector (.date) — handles most natural-language formats | Regex fallback for 'Thursday June 12th at 2 PM' |
| Time Zone | Regex abbreviation map (EST→America/New_York, AEST→Australia/Sydney, etc.) | Device local time zone |
| Company | Regex: 'at/with/from [the] [Org] team' | Linked Job.company field |
| Meeting URL | Whitespace-tokenise → URL(string:) → match zoom/meet/teams prefix | meetingPlatform = 'phone' |
| Role | Linked Job.title (not extracted from email) | — |
Design notes
→
Always show a confirmation step — Never auto-create a calendar event without user review. The parsed result has variable confidence — show all extracted fields, allow edits, and require an explicit tap to commit.
→
Confidence indicator prevents false trust — Classify confidence as High (date + time + meeting URL all found), Medium (date or time found, but not both), or Low (no date detected). Surface this visually so the user knows to inspect Low results carefully.
→
Job matcher by fuzzy name — Filter tracked jobs by company name similarity (lowercased contains-match is sufficient). Show top 3 suggestions. Always include a 'None / New Job' option to avoid forcing a bad match.
→
Share Extension is the primary entry point — The user shares plain text from Mail.app or any email client. The extension parses the text and writes a PendingInterviewEntry to the App Group container. The main app reads and clears this entry on next launch.
→
Paste fallback for in-app use — Also expose a 'Parse Email' toolbar button inside JobDetailView. This opens a plain text-paste sheet, feeds it to the same InterviewParser, and presents InterviewConfirmationSheet directly — no Share Extension needed.
Implementation
InterviewParser.ts
tsx
// Web reference — Interview email parser (TypeScript)
export interface ParsedInterview {
company?: string;
dateTime?: Date;
timeZoneName?: string;
meetingURL?: string;
meetingPlatform?: "zoom" | "meet" | "teams" | "phone";
rawText: string;
}
const PLATFORM_PATTERNS: [RegExp, "zoom" | "meet" | "teams"][] = [
[/zoom\.us\/j\/[\d]+/i, "zoom"],
[/meet\.google\.com\/[a-z-]+/i, "meet"],
[/teams\.microsoft\.com\/l\/meetup/i, "teams"],
];
const TZ_MAP: Record<string, string> = {
EST: "America/New_York", PST: "America/Los_Angeles",
CST: "America/Chicago", MST: "America/Denver",
GMT: "Europe/London", UTC: "UTC",
AEST: "Australia/Sydney", SGT: "Asia/Singapore",
};
export function parseInterviewEmail(text: string): ParsedInterview {
const result: ParsedInterview = { rawText: text };
// Company
const compMatch = text.match(/(?:at|with) the ([A-Z][A-Za-z ]+?) team/);
if (compMatch) result.company = compMatch[1];
// Meeting URL
for (const [pattern, platform] of PLATFORM_PATTERNS) {
const match = text.match(pattern);
if (match) {
result.meetingURL = `https://${match[0]}`;
result.meetingPlatform = platform;
break;
}
}
if (!result.meetingPlatform) result.meetingPlatform = "phone";
// Time zone
const tzMatch = text.match(/\b(EST|PST|CST|MST|GMT|UTC|AEST|SGT)\b/);
if (tzMatch) result.timeZoneName = TZ_MAP[tzMatch[1]] ?? tzMatch[1];
// Date — naive extraction, production should use date-fns or chrono-node
const dateMatch = text.match(
/(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)[,\s]+\w+ \d{1,2}(?:th|st|nd|rd)?/i
);
const timeMatch = text.match(/\d{1,2}(?::\d{2})?\s*(?:AM|PM)/i);
if (dateMatch && timeMatch) {
result.dateTime = new Date(`${dateMatch[0]} ${timeMatch[0]}`);
}
return result;
}Privacy & entitlements
| Key | Platform | Required for |
|---|---|---|
| NSCalendarsWriteOnlyAccessUsageDescription | iOS 17+ | EKEventStore.requestWriteOnlyAccessToEvents |
| NSCalendarsUsageDescription | macOS | EKEventStore.requestAccess(to: .event) |
| com.apple.security.personal-information.calendars | macOS entitlement | Hardened runtime calendar access |
Accessibility
- →The confirmation sheet must be presented as a modal with accessibilityViewIsModal(true) so VoiceOver does not read underlying content.
- →Each parsed field row should have an accessibilityLabel combining the field name and value: 'Date and time: Thursday June 12 at 2 PM EST'.
- →The confidence indicator must not rely on colour alone — include the word (High / Medium / Low) in the accessible label.
- →The 'Add to Calendar' button should briefly announce success via UIAccessibility.post(notification: .announcement, argument: 'Interview added to calendar') after the event is saved.