116 lines
3.6 KiB
JavaScript
116 lines
3.6 KiB
JavaScript
// @ts-check
|
|
// Pure, side-effect-free Gmail helpers. Ported verbatim from the gmail-helpers
|
|
// contributed by @SparshGarg999 in #1203 (with thanks). Files prefixed with _
|
|
// are never discovered as plugins.
|
|
|
|
/**
|
|
* Extract all http/https URLs from a string (plain text or HTML). Normalizes
|
|
* & and strips trailing punctuation. Dedups.
|
|
* @param {string} body
|
|
* @returns {string[]}
|
|
*/
|
|
export function extractUrls(body) {
|
|
if (!body) return [];
|
|
const urls = [];
|
|
const regex = /https?:\/\/[^\s"'<>\(\)]+/gi;
|
|
let match;
|
|
while ((match = regex.exec(body)) !== null) {
|
|
const url = match[0].replace(/[.,;:!?]+$/, '').replace(/&/g, '&');
|
|
urls.push(url);
|
|
}
|
|
return [...new Set(urls)];
|
|
}
|
|
|
|
/**
|
|
* Is a URL clean and relevant (not a click tracker, unsubscribe link, or pixel)?
|
|
* @param {string} url
|
|
* @returns {boolean}
|
|
*/
|
|
export function isCleanUrl(url) {
|
|
try {
|
|
const u = new URL(url);
|
|
const lowerUrl = url.toLowerCase();
|
|
const badKeywords = [
|
|
'click', 'track', 'openpixel', 'sendgrid', 'unsubscribe', 'optout',
|
|
'newsletter', 'subscribe', 'w3.org', 'doubleclick', 'googlesyndication',
|
|
'googleadservices', 'mailgun', 'mandrill', 'mjml', 'github.com/login',
|
|
'linkedin.com/legal', 'linkedin.com/help', 'linkedin.com/settings',
|
|
];
|
|
if (badKeywords.some(kw => lowerUrl.includes(kw))) return false;
|
|
return u.protocol === 'https:';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DMARC alignment check (anti-spoof gate, fail-closed). Only emails whose
|
|
* Authentication-Results header reports dmarc=pass are trusted.
|
|
* @param {Array<{ name: string, value: string }>} headers
|
|
* @returns {boolean}
|
|
*/
|
|
export function isAuthenticEmail(headers) {
|
|
if (!Array.isArray(headers)) return false;
|
|
for (const h of headers) {
|
|
if (h.name && h.name.toLowerCase() === 'authentication-results') {
|
|
if (h.value && /dmarc=pass/i.test(h.value)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Parse "{Role} at {Company}" from a subject line.
|
|
* @param {string} subject
|
|
* @returns {{ role: string, company: string } | null}
|
|
*/
|
|
export function parseRoleAtCompany(subject) {
|
|
if (!subject) return null;
|
|
let clean = subject.replace(/^(re|fwd|new match|job alert|alert|match|notification|alert for|daily alert for):\s*/i, '').trim();
|
|
clean = clean.split(/\s+[-|]\s+/)[0].trim();
|
|
const match = clean.match(/^(.+?)\s+at\s+(.+)$/i);
|
|
if (match) {
|
|
const role = match[1].trim();
|
|
const company = match[2].trim();
|
|
if (role && company && role.length < 100 && company.length < 100) {
|
|
return { role, company };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Recursively decode a Gmail message payload's base64url body parts to text.
|
|
* @param {any} payload
|
|
* @returns {string}
|
|
*/
|
|
export function getMessageBody(payload) {
|
|
if (!payload) return '';
|
|
let body = '';
|
|
if (payload.body && payload.body.data) {
|
|
const base64 = payload.body.data.replace(/-/g, '+').replace(/_/g, '/');
|
|
body += Buffer.from(base64, 'base64').toString('utf-8');
|
|
}
|
|
if (payload.parts) {
|
|
for (const part of payload.parts) body += getMessageBody(part);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
/**
|
|
* Best-effort company name from a known ATS URL (greenhouse/lever slug).
|
|
* @param {string} url
|
|
* @returns {string}
|
|
*/
|
|
export function companyFromUrl(url) {
|
|
try {
|
|
const { hostname, pathname } = new URL(url);
|
|
if (hostname === 'boards.greenhouse.io' || hostname.endsWith('.greenhouse.io') ||
|
|
hostname === 'jobs.lever.co' || hostname.endsWith('.lever.co')) {
|
|
const parts = pathname.split('/').filter(Boolean);
|
|
if (parts.length > 0) return parts[0];
|
|
}
|
|
} catch { /* malformed → no company */ }
|
|
return '';
|
|
}
|