8 Commits

Author SHA1 Message Date
KS Jannette
28a368326d more 2026-02-18 06:21:04 -05:00
S Jannette
50214fb4e5 Merge pull request #3 from kjannette/processEmails
Ingest and process emails, take calendar actions, build rich spreadsheets.
2026-02-18 06:12:25 -05:00
KS Jannette
dc1fdecfcd More 2026-02-18 06:06:17 -05:00
KS Jannette
e66eea75dc more 2026-02-18 05:48:50 -05:00
S Jannette
54d944b9fb Merge pull request #2 from kjannette/sec
Sec
2026-02-17 14:00:22 -05:00
KS Jannette
31be44a3b9 more 2026-02-17 13:56:02 -05:00
KS Jannette
4192bb9dee more 2026-02-17 13:53:48 -05:00
S Jannette
5c29eec50b Merge pull request #1 from kjannette/addSecondaryAccount
more
2026-02-17 13:32:52 -05:00
7 changed files with 463 additions and 76 deletions

2
.gitignore vendored
View File

@@ -9,4 +9,4 @@ accounts.json
# Runtime output
summary.json
token-secondary.json
summary-secondary.json

View File

@@ -10,7 +10,7 @@ Infinitely mod-able. Dead simple. Privacy centric.
# Scope
## Current implementation contemplates: Claude Desktop + Local Model Context Protocol Server + Commercial SMTP Server (MX'configd properly) + your DNS-configd XXX.YYY
## Current implementation contemplates: Claude Desktop + Custom, Local Model Context Protocol Server + Commercial SMTP Server (MX'configd properly) + your DNS-configd XXX.YYY
---

View File

@@ -1,30 +0,0 @@
You are an expert Executive Assistant AI specialized in email management and productivity. Your goal is to organize, prioritize, and summarize incoming emails to maximize user efficiency.
I have been applying for jobs, which generates a large volume of responses. Review my inbox and apply the following instructions to all new emails.
STEP 1: Constraints
- Do not make up information.
STEP 2: CLASSIFY each email into exactly one category.
Category A - Acknowledgement Only
The employer confirms receipt of my application but requires no action from me. Typical language: "We received your application," "Your application is under review," "We will contact you if selected." No next steps are requested.
Category B - Advancement to Next Step
The employer wants to move forward. This includes: invitations to schedule a phone screen or interview, requests to complete an assessment or assignment, requests for additional information or documents, or any communication that requires a response or action from me. If an email both acknowledges receipt AND requests action, classify it as B.
Category C - Rejection
The employer declines to move forward. Typical language: "We have decided to pursue other candidates," "Unfortunately, you were not selected," "We will not be moving forward with your application."
Category D - Other
The email does not fit into categories A, B, or C. This includes non-job-application emails, newsletters, promotional content, or ambiguous messages that do not clearly match another category.
STEP 3: DELETE emails classified as Category A and Category C.
STEP 4: SUMMARIZE emails classified as Category B and Category D. For each, include:
1. Sender name and email address
2. Email subject line
3. Date and time received
4. Category (B or D)
5. Suggested action I should take

View File

@@ -6,18 +6,43 @@ import readline from "readline";
const __dirname = path.dirname(new URL(import.meta.url).pathname);
const PROJECT_ROOT = path.resolve(__dirname, "..");
const CREDENTIALS_PATH = path.join(PROJECT_ROOT, "credentials.json");
const TOKEN_PATH = path.join(PROJECT_ROOT, "token.json");
const ACCOUNTS_PATH = path.join(PROJECT_ROOT, "accounts.json");
// Gmail modify scope: read, send, delete, and manage labels
const SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];
const SCOPES = [
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/calendar.events",
];
function resolveTokenPath(accountKey: string): string {
if (!fs.existsSync(ACCOUNTS_PATH)) {
throw new Error(`Missing ${ACCOUNTS_PATH}. Create accounts.json first.`);
}
const accounts = JSON.parse(fs.readFileSync(ACCOUNTS_PATH, "utf-8"));
const acct = accounts[accountKey];
if (!acct) {
const available = Object.keys(accounts).join(", ");
throw new Error(
`Unknown account "${accountKey}". Available: ${available}`
);
}
return path.join(PROJECT_ROOT, acct.tokenFile);
}
async function authorize(): Promise<void> {
const accountKey = process.argv[2] || "work";
const tokenPath = resolveTokenPath(accountKey);
console.log(`Authorizing account: "${accountKey}"`);
console.log(`Token file: ${tokenPath}`);
console.log(`Scopes: ${SCOPES.join(", ")}\n`);
if (!fs.existsSync(CREDENTIALS_PATH)) {
console.error(
`Missing ${CREDENTIALS_PATH}\n\n` +
"To create this file:\n" +
"1. Go to https://console.cloud.google.com/\n" +
"2. Create a project and enable the Gmail API\n" +
"2. Create a project and enable the Gmail, Sheets, and Calendar APIs\n" +
"3. Create OAuth 2.0 credentials (Desktop app type)\n" +
"4. Download the JSON and save it as credentials.json in the project root\n"
);
@@ -34,16 +59,18 @@ async function authorize(): Promise<void> {
redirect_uris[0]
);
// Check if we already have a valid token
if (fs.existsSync(TOKEN_PATH)) {
const token = JSON.parse(fs.readFileSync(TOKEN_PATH, "utf-8"));
if (fs.existsSync(tokenPath)) {
const token = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
oAuth2Client.setCredentials(token);
console.log("Token already exists at:", TOKEN_PATH);
console.log("To re-authorize, delete token.json and run this script again.");
console.log("Token already exists at:", tokenPath);
console.log(
"To re-authorize with new scopes, delete the token file and run again:\n" +
` rm ${tokenPath}\n` +
` npm run auth -- ${accountKey}`
);
return;
}
// Generate auth URL and prompt user
const authUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: SCOPES,
@@ -69,7 +96,6 @@ async function authorize(): Promise<void> {
);
});
// Extract the code whether they pasted the full URL or just the code
let code = rawInput;
if (rawInput.includes("code=")) {
const url = new URL(rawInput);
@@ -81,8 +107,8 @@ async function authorize(): Promise<void> {
const { tokens } = await oAuth2Client.getToken(code);
oAuth2Client.setCredentials(tokens);
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
console.log("\nToken saved to:", TOKEN_PATH);
fs.writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));
console.log("\nToken saved to:", tokenPath);
console.log("You can now start the MCP server.");
}

View File

@@ -1,7 +1,8 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { google, gmail_v1 } from "googleapis";
import { google, gmail_v1, sheets_v4, calendar_v3 } from "googleapis";
import { OAuth2Client } from "google-auth-library";
import fs from "fs";
import path from "path";
@@ -13,7 +14,10 @@ const __dirname = path.dirname(new URL(import.meta.url).pathname);
const PROJECT_ROOT = path.resolve(__dirname, "..");
const CREDENTIALS_PATH = path.join(PROJECT_ROOT, "credentials.json");
const ACCOUNTS_PATH = path.join(PROJECT_ROOT, "accounts.json");
const PROMPT_PATH = path.join(PROJECT_ROOT, "classify-emails.txt");
const CLASSIFY_PROMPT_PATH = path.join(PROJECT_ROOT, "classify-emails.txt");
const ACTION_PROMPT_PATH = path.join(
PROJECT_ROOT, "src", "prompts", "take-action-on-emails.txt"
);
// ---------------------------------------------------------------------------
// Account configuration
@@ -21,6 +25,8 @@ const PROMPT_PATH = path.join(PROJECT_ROOT, "classify-emails.txt");
interface AccountConfig {
label: string;
tokenFile: string;
spreadsheetId?: string;
calendarId?: string;
}
interface AccountsMap {
@@ -54,27 +60,37 @@ function getSummaryPath(account: string): string {
}
const VALID_ACCOUNTS = ["work", "secondary"] as const;
const accounts = loadAccounts();
const accountDescription = VALID_ACCOUNTS
.map((key) => `"${key}" (${accounts[key]?.label ?? key})`)
.join(" or ");
const accountSchema = z
.enum(VALID_ACCOUNTS)
.describe(
"Which email account to use: \"work\" (sj@sjdev.co) or \"secondary\" (ken.jannette@gmail.com)"
);
.describe(`Which email account to use: ${accountDescription}`);
// ---------------------------------------------------------------------------
// Load classification prompt
// Prompt loaders
// ---------------------------------------------------------------------------
function loadClassificationPrompt(): string {
if (!fs.existsSync(PROMPT_PATH)) {
console.error(`Warning: ${PROMPT_PATH} not found. Classification instructions will be missing.`);
function loadPromptFile(filePath: string, label: string): string {
if (!fs.existsSync(filePath)) {
console.error(`Warning: ${filePath} not found. ${label} will be missing.`);
return "";
}
return fs.readFileSync(PROMPT_PATH, "utf-8");
return fs.readFileSync(filePath, "utf-8");
}
function loadClassificationPrompt(): string {
return loadPromptFile(CLASSIFY_PROMPT_PATH, "Classification instructions");
}
function loadActionPrompt(): string {
return loadPromptFile(ACTION_PROMPT_PATH, "Action instructions");
}
// ---------------------------------------------------------------------------
// Gmail auth helper — parameterized by account key
// Auth helpersgeneric OAuth2 client, then service-specific factories
// ---------------------------------------------------------------------------
function getGmailClient(account: string): gmail_v1.Gmail {
function getOAuth2Client(account: string): OAuth2Client {
const tokenPath = getTokenPath(account);
if (!fs.existsSync(CREDENTIALS_PATH)) {
@@ -101,7 +117,6 @@ function getGmailClient(account: string): gmail_v1.Gmail {
const token = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
oAuth2Client.setCredentials(token);
// Persist refreshed tokens automatically
oAuth2Client.on("tokens", (newTokens) => {
const current = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
fs.writeFileSync(
@@ -111,7 +126,33 @@ function getGmailClient(account: string): gmail_v1.Gmail {
console.error(`Token refreshed and saved for account "${account}".`);
});
return google.gmail({ version: "v1", auth: oAuth2Client });
return oAuth2Client;
}
function getGmailClient(account: string): gmail_v1.Gmail {
return google.gmail({ version: "v1", auth: getOAuth2Client(account) });
}
function getSheetsClient(account: string): sheets_v4.Sheets {
return google.sheets({ version: "v4", auth: getOAuth2Client(account) });
}
function getCalendarClient(account: string): calendar_v3.Calendar {
return google.calendar({ version: "v3", auth: getOAuth2Client(account) });
}
function getSpreadsheetId(account: string): string {
const acct = accounts[account];
if (!acct?.spreadsheetId || acct.spreadsheetId === "PASTE_YOUR_SHEET_ID_HERE") {
throw new Error(
`No spreadsheetId configured for account "${account}" in accounts.json.`
);
}
return acct.spreadsheetId;
}
function getCalendarId(account: string): string {
return accounts[account]?.calendarId ?? "primary";
}
// ---------------------------------------------------------------------------
@@ -169,10 +210,11 @@ server.registerPrompt(
"review_emails",
{
description:
"Review WORK inbox (sj@sjdev.co): classify job application emails (A/B/C/D), delete A+C, summarize B+D.",
`Review WORK inbox (${accounts.work?.label ?? "work"}): classify emails, delete A+C, summarize B+D, log B to spreadsheet, create calendar events.`,
},
() => {
const instructions = loadClassificationPrompt();
const phase1 = loadClassificationPrompt();
const phase2 = loadActionPrompt();
return {
messages: [
{
@@ -181,8 +223,8 @@ server.registerPrompt(
type: "text" as const,
text:
`ACCOUNT: Use account = "work" for ALL tool calls in this session.\n\n` +
(instructions ||
"Review my new emails and classify them by job application category."),
(phase1 || "Review my new emails and classify them by job application category.") +
(phase2 ? `\n\n${phase2}` : ""),
},
},
],
@@ -197,10 +239,11 @@ server.registerPrompt(
"review_secondary_emails",
{
description:
"Review SECONDARY inbox (ken.jannette@gmail.com): classify job application emails (A/B/C/D), delete A+C, summarize B+D.",
`Review SECONDARY inbox (${accounts.secondary?.label ?? "secondary"}): classify emails, delete A+C, summarize B+D, log B to spreadsheet, create calendar events.`,
},
() => {
const instructions = loadClassificationPrompt();
const phase1 = loadClassificationPrompt();
const phase2 = loadActionPrompt();
return {
messages: [
{
@@ -209,8 +252,8 @@ server.registerPrompt(
type: "text" as const,
text:
`ACCOUNT: Use account = "secondary" for ALL tool calls in this session.\n\n` +
(instructions ||
"Review my new emails and classify them by job application category."),
(phase1 || "Review my new emails and classify them by job application category.") +
(phase2 ? `\n\n${phase2}` : ""),
},
},
],
@@ -286,9 +329,14 @@ server.registerTool(
}
const classificationInstructions = loadClassificationPrompt();
const instructionsBlock = classificationInstructions
? `\n\n${"=".repeat(60)}\nCLASSIFICATION INSTRUCTIONS:\n${"=".repeat(60)}\n${classificationInstructions}`
: "";
const actionInstructions = loadActionPrompt();
const instructionsBlock =
(classificationInstructions
? `\n\n${"=".repeat(60)}\nCLASSIFICATION INSTRUCTIONS (PHASE 1):\n${"=".repeat(60)}\n${classificationInstructions}`
: "") +
(actionInstructions
? `\n\n${"=".repeat(60)}\nACTION INSTRUCTIONS (PHASE 2):\n${"=".repeat(60)}\n${actionInstructions}`
: "");
return {
content: [
@@ -423,15 +471,30 @@ server.registerTool(
summary = JSON.parse(fs.readFileSync(summaryPath, "utf-8"));
}
const now = new Date().toISOString();
const now = new Date();
const nowIso = now.toISOString();
const newEntries: SummaryEntry[] = entries.map((e) => ({
...e,
addedAt: now,
addedAt: nowIso,
}));
summary.push(...newEntries);
// Purge entries older than 30 days
const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
const cutoff = now.getTime() - RETENTION_MS;
const beforePurge = summary.length;
summary = summary.filter(
(entry) => new Date(entry.addedAt).getTime() >= cutoff
);
const purged = beforePurge - summary.length;
fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2));
const purgeNote = purged > 0
? `\nPurged ${purged} entry/entries older than 30 days.`
: "";
return {
content: [
{
@@ -439,7 +502,8 @@ server.registerTool(
text:
`Appended ${newEntries.length} entry/entries to summary.\n` +
`Total entries in summary: ${summary.length}\n` +
`Summary file: ${summaryPath}`,
`Summary file: ${summaryPath}` +
purgeNote,
},
],
};
@@ -457,6 +521,287 @@ server.registerTool(
}
);
// ---------------------------------------------------------------------------
// Sheet column layout for "Recruiter Communication Log and Call Schedule"
// ---------------------------------------------------------------------------
const SHEET_COLUMNS = [
"Recruiter Name", // A
"Recruiter Email", // B
"Recruiter Tel", // C
"Company/Role", // D
"First Contact", // E
"Subsequent Contact(s)", // F
"Recruiter Call Scheduled", // G
"Company Contact Info", // H
"Company First Interview", // I
"Company Second Interview", // J
];
const SHEET_RANGE = "Sheet1";
// ---------------------------------------------------------------------------
// Tool: log_recruiter_contact
// ---------------------------------------------------------------------------
server.registerTool(
"log_recruiter_contact",
{
description:
"Log or update recruiter contact info in the tracking spreadsheet. " +
"If a row already exists for the same recruiterEmail + companyRole, it " +
"updates the existing row (merging non-empty fields). Otherwise appends " +
"a new row. Use for Category B emails after classification.",
inputSchema: {
account: accountSchema,
recruiterName: z.string().describe("Recruiter's full name"),
recruiterEmail: z.string().describe("Recruiter's email address"),
recruiterTel: z
.string()
.optional()
.describe("Recruiter's phone number, if mentioned in the email"),
companyRole: z
.string()
.describe("Company name and role the recruiter seeks to fill"),
firstContact: z
.string()
.describe("Date/time of first contact (from the email's Date header)"),
subsequentContacts: z
.string()
.optional()
.describe("Any follow-up contact context mentioned in the email"),
recruiterCallScheduled: z
.string()
.optional()
.describe(
"Scheduled call details: date, time, platform (Zoom/Teams), " +
"meeting link or phone, and whether they have your cell number"
),
companyContactInfo: z
.string()
.optional()
.describe("Company interviewer name/email/tel if advancing to company interview"),
companyFirstInterview: z
.string()
.optional()
.describe("Company first interview: date, time, platform, link/phone, cell number note"),
companySecondInterview: z
.string()
.optional()
.describe("Company second interview: date, time, platform, link/phone, cell number note"),
},
},
async ({
account,
recruiterName,
recruiterEmail,
recruiterTel,
companyRole,
firstContact,
subsequentContacts,
recruiterCallScheduled,
companyContactInfo,
companyFirstInterview,
companySecondInterview,
}) => {
try {
const sheets = getSheetsClient(account);
const spreadsheetId = getSpreadsheetId(account);
const incomingRow = [
recruiterName,
recruiterEmail,
recruiterTel ?? "",
companyRole,
firstContact,
subsequentContacts ?? "",
recruiterCallScheduled ?? "",
companyContactInfo ?? "",
companyFirstInterview ?? "",
companySecondInterview ?? "",
];
// Read existing data to check for a matching row
const existing = await sheets.spreadsheets.values.get({
spreadsheetId,
range: SHEET_RANGE,
});
const rows = existing.data.values ?? [];
// Find row matching recruiterEmail (col B) + companyRole (col D)
const emailNorm = recruiterEmail.toLowerCase().trim();
const roleNorm = companyRole.toLowerCase().trim();
let matchIdx = -1;
for (let i = 0; i < rows.length; i++) {
const rowEmail = (rows[i][1] ?? "").toString().toLowerCase().trim();
const rowRole = (rows[i][3] ?? "").toString().toLowerCase().trim();
if (rowEmail === emailNorm && rowRole === roleNorm) {
matchIdx = i;
break;
}
}
if (matchIdx >= 0) {
// Merge: keep existing values where incoming is empty
const existingRow = rows[matchIdx];
const merged = incomingRow.map((val, col) => {
if (col === 5 && val && existingRow[col]) {
// Subsequent Contacts: append rather than overwrite
return `${existingRow[col]}; ${val}`;
}
return val || existingRow[col] || "";
});
const rowNum = matchIdx + 1; // 1-indexed
await sheets.spreadsheets.values.update({
spreadsheetId,
range: `${SHEET_RANGE}!A${rowNum}:J${rowNum}`,
valueInputOption: "USER_ENTERED",
requestBody: { values: [merged] },
});
return {
content: [
{
type: "text" as const,
text: `Updated existing row ${rowNum} for ${recruiterName} / ${companyRole}.`,
},
],
};
}
// No match — append new row
await sheets.spreadsheets.values.append({
spreadsheetId,
range: SHEET_RANGE,
valueInputOption: "USER_ENTERED",
requestBody: { values: [incomingRow] },
});
return {
content: [
{
type: "text" as const,
text: `Appended new row for ${recruiterName} / ${companyRole}.`,
},
],
};
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text" as const,
text: `Error logging recruiter contact: ${errMsg}`,
},
],
};
}
}
);
// ---------------------------------------------------------------------------
// Tool: create_calendar_event
// ---------------------------------------------------------------------------
server.registerTool(
"create_calendar_event",
{
description:
"Create a Google Calendar event. Use immediately after logging a " +
"recruiter call, company first interview, or company second interview " +
"to the spreadsheet. One calendar event per scheduling column populated.",
inputSchema: {
account: accountSchema,
title: z
.string()
.describe(
"Event title, e.g. '[Recruiter Call] Acme Corp - Sr Engineer' " +
"or '[Interview] Acme Corp - Sr Engineer'"
),
startDateTime: z
.string()
.describe("Event start in ISO 8601 format, e.g. 2026-02-20T14:00:00-06:00"),
durationMinutes: z
.number()
.min(5)
.max(480)
.optional()
.describe("Duration in minutes (default 60)"),
description: z
.string()
.optional()
.describe(
"Event description: recruiter/company contact info, notes, " +
"whether they have your cell number, etc."
),
location: z
.string()
.optional()
.describe("Video meeting link (Zoom/Teams URL) or phone number"),
},
},
async ({ account, title, startDateTime, durationMinutes, description, location }) => {
try {
const start = new Date(startDateTime);
if (start.getTime() < Date.now()) {
return {
content: [
{
type: "text" as const,
text:
`Skipped calendar event "${title}" — the proposed date ` +
`(${start.toISOString()}) is in the past.`,
},
],
};
}
const calendar = getCalendarClient(account);
const calendarId = getCalendarId(account);
const end = new Date(start.getTime() + (durationMinutes ?? 60) * 60_000);
const event = await calendar.events.insert({
calendarId,
requestBody: {
summary: title,
start: {
dateTime: start.toISOString(),
},
end: {
dateTime: end.toISOString(),
},
description: description ?? "",
location: location ?? "",
},
});
return {
content: [
{
type: "text" as const,
text:
`Created calendar event: "${title}"\n` +
`Start: ${start.toISOString()}\n` +
`End: ${end.toISOString()}\n` +
`Event ID: ${event.data.id}\n` +
`Link: ${event.data.htmlLink}`,
},
],
};
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text" as const,
text: `Error creating calendar event: ${errMsg}`,
},
],
};
}
}
);
// ---------------------------------------------------------------------------
// Start server
// ---------------------------------------------------------------------------

View File

@@ -21,10 +21,11 @@ The email does not fit into categories A, B, or C. This includes non-job-applica
STEP 3: DELETE emails classified as Category A and Category C.
STEP 4: SUMMARIZE emails classified as Category B and Category D. For each, include:
STEP 4: ("Anti step") - NEVER DELETE Category B emails.
STEP 5: SUMMARIZE emails classified as Category B and Category D. For each, include:
1. Sender name and email address
2. Email subject line
3. Date and time received
4. Category (B or D)
5. Suggested action I should take

View File

@@ -0,0 +1,45 @@
PHASE 2: TAKE ACTION ON CATEGORY B EMAILS
After completing classification (Phase 1), perform the following actions for each email classified as Category B (Advancement to Next Step).
STEP 1: LOG TO SPREADSHEET
For each Category B email, extract as much of the following information as possible from the email contents and log it using the log_recruiter_contact tool:
A column exists for:
- Recruiter Name
- Recruiter Email
- Recruiter Phone/Tel
- Company/Role the recruiter seeks to fill
- First Contact: the date and time the email was received
- Subsequent Contacts: any follow-up communication with this recruiter about this role
- Recruiter Call Scheduled: if the email contains scheduling information, extract ALL details — date, time, platform (Zoom, MS Teams, Google Meet, etc.), meeting link, phone number to call — and note whether they confirm having your cell number or might not have it
A row should already exists for the same recruiter email + company/role combination (from a previous email), the tool will UPDATE the existing row. Provide all available fields and the tool handles merging.
STEP 2: CREATE CALENDAR EVENTS
Immediately after logging to the spreadsheet, check whether ANY of these fields were populated or updated:
- Recruiter Call Scheduled
- Company First Interview
- Company Second Interview
For EACH of the above fields that contains a date/time, create a Google Calendar event using the create_calendar_event tool:
- Title format: "[<Recruiter NaME> Call] Company - Role" or "[First Interview] Company - Role" or "[Second Interview] Company - Role"
- Start date/time: extracted from the email (convert to ISO 8601)
- Duration: 30 minutes unless otherwise specified in the email
- Location: the video meeting link, phone number, or platform name
- Description: include recruiter or company contact name/email/phone, and note whether they have your cell number
STEP 3: COMPANY INTERVIEW TRACKING
When an email indicates advancement beyond the recruiter screen to a company interview:
- Populate the Company First Interview or Company Second Interview field with details
- Those details should include company contact info (interviewer name, email, phone)
- Then immeidately create a calendar event for the new interview date/time including interviewer name, email, phone
STRICT CONSTRAINTS:
- Do not make up phone numbers, links, dates, or anything at all. If it does not exist in text wihtin a verifiable email, IT DOES NOT EXIST.
- PAST-DATE CHECK: Before creating a calendar event, compare the proposed meeting date/time against today's date. If the meeting date has already passed, do NOT create a calendar event for it. Still log it to the spreadsheet, but skip the calendar step. The tool will also enforce this server-side.
- As described in more detail below, each new date/time value in a scheduling column = one new calendar event. Never skip this.
- Always use the same account for calendar events as was used for email fetching.
- Do not create duplicate calendar events for the same meeting.
- If information is not present in the email, leave that field empty do ot guess.