Skip to main content
Glama
digest.js•16.7 kB
/** * Digest email functionality for job matches * Reimplementation of src/mailer.js for Cloudflare Workers */ import nodemailer from 'nodemailer'; /** * Format date with configured timezone * @param {Date} date - Date to format * @param {Object} env - Environment variables * @param {Object} options - Formatting options * @returns {string} - Formatted date string */ function formatDateWithTimezone(date = new Date(), env, options = {}) { const { dateStyle = 'short', timeStyle = 'short', includeTime = true } = options; const timezone = env.TIMEZONE || 'UTC'; try { if (includeTime) { return date.toLocaleString('en-US', { timeZone: timezone, dateStyle, timeStyle }); } else { return date.toLocaleDateString('en-US', { timeZone: timezone, dateStyle }); } } catch (error) { console.warn(`Invalid timezone '${timezone}', falling back to UTC`); if (includeTime) { return date.toLocaleString('en-US', { timeZone: 'UTC', dateStyle, timeStyle }); } else { return date.toLocaleDateString('en-US', { timeZone: 'UTC', dateStyle }); } } } /** * Check if SMTP is configured * @param {Object} env - Environment variables * @returns {Object} - Configuration status and missing variables */ export function checkSmtpConfiguration(env) { const requiredVars = ['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS']; const missingVars = requiredVars.filter(varName => !env[varName]); return { isConfigured: missingVars.length === 0, missingVars }; } /** * Get jobs that should be included in digest (completed scans, not previously sent) * @param {Object} env - Environment variables (for KV storage access) * @returns {Array} - Array of jobs to include in digest */ export async function getJobsForDigest(env) { try { const jobIndex = await env.JOB_STORAGE.get('job_index', 'json'); if (!jobIndex || !jobIndex.jobs) return []; return jobIndex.jobs.filter(job => { // Only include scanned jobs with completed status if (!job.scanned || job.scanStatus !== 'completed') return false; // Only include jobs not previously sent in digest if (job.sentInDigest) return false; return true; }); } catch (error) { console.error('Error getting jobs for digest:', error); return []; } } /** * Mark jobs as sent in digest * @param {Object} env - Environment variables (for KV storage access) * @returns {number} - Number of jobs marked as sent */ export async function markJobsAsSent(env) { try { const jobIndex = await env.JOB_STORAGE.get('job_index', 'json'); if (!jobIndex || !jobIndex.jobs) return 0; // Mark all completed, unsent jobs as sent let markedCount = 0; jobIndex.jobs.forEach(job => { if (job.scanned && job.scanStatus === 'completed' && !job.sentInDigest) { job.sentInDigest = true; job.digestSentDate = new Date().toISOString(); markedCount++; } }); if (markedCount > 0) { jobIndex.lastUpdate = new Date().toISOString(); await env.JOB_STORAGE.put('job_index', JSON.stringify(jobIndex)); console.log(`Marked ${markedCount} jobs as sent in digest`); } return markedCount; } catch (error) { console.error('Error marking jobs as sent:', error); return 0; } } /** * Filter jobs based on digest criteria * @param {Array} jobs - Array of jobs to filter * @param {Object} criteria - Filter criteria * @param {boolean} criteria.onlyNew - Only include jobs not previously sent * @param {number} criteria.minMatchScore - Minimum match score threshold * @returns {Array} - Filtered jobs */ export function filterJobsForDigest(jobs, criteria = {}) { const { onlyNew = true, minMatchScore = 0.0 } = criteria; return jobs.filter(job => { // Only include scanned jobs with completed status if (!job.scanned || job.scanStatus !== 'completed') return false; // Apply match score filter if (job.matchScore < minMatchScore) return false; // Apply onlyNew filter if (onlyNew && job.sentInDigest) return false; return true; }); } /** * Generate HTML email content for job digest * @param {Array} jobs - Array of jobs to include * @param {Object} env - Environment variables (for timezone) * @param {Object} options - Email options * @param {string} options.source - Source of the digest (scan, rescan, etc.) * @param {boolean} options.onlyNew - Whether only new jobs are included * @param {string} options.error - Error message for failed scans * @returns {string} - HTML email content */ export function generateDigestHtml(jobs, env, options = {}) { const { source, onlyNew, error } = options; const sourceText = source ? ` from ${source}` : ''; // Handle scan failure notifications if (source === 'scan_failed') { return ` <h2 style="color: #d32f2f;">Scan Failed${sourceText}</h2> <div style="background-color: #ffebee; padding: 15px; border-left: 4px solid #d32f2f; margin: 10px 0;"> <p><strong>Error:</strong> ${error || 'Unknown error occurred'}</p> </div> <p>The scheduled job search scan encountered an error and could not complete successfully.</p> <p>Please check the system logs and configuration to resolve this issue.</p> <p><em>Generated on ${formatDateWithTimezone(new Date(), env)}</em></p> `; } // Handle normal job digest return ` <h2>Job Matches${sourceText}</h2> <p>Found ${jobs.length} potential job matches${onlyNew ? ' (new)' : ''}:</p> <table border="1" cellpadding="5" style="border-collapse: collapse; width: 100%;"> <tr style="background-color: #f2f2f2;"> <th>Title</th> <th>Company</th> <th>Match Score</th> <th>Location</th> <th>Match Reason</th> </tr> ${jobs.map(job => ` <tr> <td><a href="${job.url}">${job.title}</a></td> <td>${job.company || 'N/A'}</td> <td>${job.matchScore ? Math.round(job.matchScore * 100) + '%' : 'N/A'}</td> <td>${job.location || 'N/A'}</td> <td>${job.matchReason || 'No reason provided'}</td> </tr> `).join('')} </table> <p><em>Generated on ${formatDateWithTimezone(new Date(), env)}</em></p> `; } /** * Send digest email using fetch to external SMTP service or Cloudflare Email API * @param {string} toEmail - Recipient email address * @param {Array} jobs - Array of jobs to include * @param {Object} env - Environment variables * @param {Object} options - Email options * @param {string} options.subject - Custom email subject * @param {string} options.source - Source of the digest * @param {boolean} options.onlyNew - Whether only new jobs are included * @param {string} options.error - Error message for failed scans * @returns {Object} - Result object with success status and message/error */ export async function sendDigestEmail(toEmail, jobs, env, options = {}) { try { const { subject, source, onlyNew, error } = options; const sourceText = source ? ` from ${source}` : ''; // Adjust subject line for failed scans let emailSubject; if (source === 'scan_failed') { emailSubject = subject || `Scan Failed - ${formatDateWithTimezone(new Date(), env, { includeTime: false })}`; } else { emailSubject = subject || `Job matches${sourceText} - ${formatDateWithTimezone(new Date(), env, { includeTime: false })}`; } // Generate HTML content const html = generateDigestHtml(jobs, env, { source, onlyNew, error }); // Create nodemailer transporter const transporter = nodemailer.createTransport({ host: env.SMTP_HOST, port: parseInt(env.SMTP_PORT), secure: env.SMTP_SECURE === 'true', auth: { user: env.SMTP_USER, pass: env.SMTP_PASS } }); // Send email using nodemailer console.log(`Sending digest email to ${toEmail} with ${jobs.length} jobs`); console.log(`Subject: ${emailSubject}`); console.log(`Jobs: ${jobs.map(j => `${j.title} at ${j.company} (${Math.round(j.matchScore * 100)}%)`).join(', ')}`); await transporter.sendMail({ from: env.SMTP_FROM || env.SMTP_USER, to: toEmail, subject: emailSubject, html: html }); console.log(`Successfully sent digest email to ${toEmail}`); return { success: true, message: `Email sent to ${toEmail}` }; } catch (error) { console.error('Error sending digest email:', error); return { success: false, error: error.message }; } } /** * Auto-send digest after scan completion * @param {Object} env - Environment variables * @param {Object} options - Digest options * @param {string} options.source - Source of the digest (scan, rescan, etc.) * @returns {Object} - Result object with success status and details */ export async function autoSendDigest(env, options = {}) { try { const { source = 'scan' } = options; // Check if DIGEST_TO is configured if (!env.DIGEST_TO) { console.log('DIGEST_TO not configured, skipping auto-digest'); return { success: false, error: 'DIGEST_TO not configured' }; } // Check SMTP configuration const smtpCheck = checkSmtpConfiguration(env); if (!smtpCheck.isConfigured) { console.log(`SMTP not configured, skipping auto-digest. Missing: ${smtpCheck.missingVars.join(', ')}`); return { success: false, error: 'SMTP not configured', missingVars: smtpCheck.missingVars }; } // Get jobs for digest const jobs = await getJobsForDigest(env); // Check if we should send digest even with zero jobs const sendOnZeroJobs = env.SEND_DIGEST_ON_ZERO_JOBS === 'true'; if (jobs.length === 0 && !sendOnZeroJobs) { console.log('No new jobs to send in auto-digest and SEND_DIGEST_ON_ZERO_JOBS is disabled'); return { success: false, error: 'No new jobs to send' }; } if (jobs.length === 0 && sendOnZeroJobs) { console.log('No new jobs found, but SEND_DIGEST_ON_ZERO_JOBS is enabled - sending empty digest'); } // Send digest email console.log(`Auto-sending digest email with ${jobs.length} jobs...`); const emailResult = await sendDigestEmail(env.DIGEST_TO, jobs, env, { source, onlyNew: true }); if (emailResult.success) { // Mark jobs as sent const markedCount = await markJobsAsSent(env); console.log(`Digest email sent successfully, marked ${markedCount} jobs as sent`); return { success: true, jobsSent: jobs.length, markedAsSent: markedCount }; } else { console.log(`Failed to send auto-digest: ${emailResult.error}`); return { success: false, error: emailResult.error }; } } catch (error) { console.error('Error in auto-send digest:', error); return { success: false, error: error.message }; } } /** * Send notification email when a scheduled trigger occurs * @param {Object} env - Environment variables * @param {Object} triggerInfo - Information about the trigger * @param {string} triggerInfo.scheduledTime - When the trigger was scheduled * @param {string} triggerInfo.cron - The cron pattern * @returns {Object} - Result object with success status and message/error */ export async function sendScheduledTriggerNotification(env, triggerInfo = {}) { try { // Check if scheduled trigger emails are enabled if (env.SCHEDULED_TRIGGER_EMAIL !== 'true') { console.log('Scheduled trigger email notifications are disabled'); return { success: false, error: 'Scheduled trigger email notifications disabled' }; } // Check if DIGEST_TO is configured if (!env.DIGEST_TO) { console.log('DIGEST_TO not configured, cannot send scheduled trigger notification'); return { success: false, error: 'DIGEST_TO not configured' }; } // Check SMTP configuration const smtpCheck = checkSmtpConfiguration(env); if (!smtpCheck.isConfigured) { console.log(`SMTP not configured, cannot send scheduled trigger notification. Missing: ${smtpCheck.missingVars.join(', ')}`); return { success: false, error: 'SMTP not configured', missingVars: smtpCheck.missingVars }; } const { scheduledTime, cron } = triggerInfo; const triggerDate = scheduledTime ? formatDateWithTimezone(new Date(scheduledTime), env) : 'Unknown'; // Create email content const subject = '🤖 Job Search Scan Started - Scheduled Trigger'; const htmlContent = ` <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;"> <h2 style="color: #2563eb; margin-bottom: 20px;">📅 Scheduled Job Search Scan Started</h2> <div style="background-color: #f8fafc; padding: 15px; border-radius: 8px; margin-bottom: 20px;"> <p style="margin: 0; color: #475569;"><strong>Trigger Time:</strong> ${triggerDate}</p> <p style="margin: 5px 0 0 0; color: #475569;"><strong>Cron Pattern:</strong> <code style="background-color: #e2e8f0; padding: 2px 4px; border-radius: 3px;">${cron || 'Unknown'}</code></p> </div> <p style="color: #475569; line-height: 1.6;">Your automated job search scan has been triggered and is now running. You'll receive a digest email with the results once the scan completes.</p> <div style="background-color: #ecfdf5; border-left: 4px solid #10b981; padding: 15px; margin: 20px 0;"> <p style="margin: 0; color: #065f46; font-weight: 500;">💡 Tip: You can disable these trigger notifications by setting <code>SCHEDULED_TRIGGER_EMAIL=false</code> in your environment variables.</p> </div> <hr style="border: none; border-top: 1px solid #e2e8f0; margin: 30px 0;"> <p style="color: #94a3b8; font-size: 14px; margin: 0;">This is an automated message from your MCP Job Search system.</p> </div> `; // Send email using nodemailer const transporter = nodemailer.createTransporter({ host: env.SMTP_HOST, port: parseInt(env.SMTP_PORT), secure: parseInt(env.SMTP_PORT) === 465, auth: { user: env.SMTP_USER, pass: env.SMTP_PASS } }); const mailOptions = { from: env.SMTP_USER, to: env.DIGEST_TO, subject: subject, html: htmlContent }; console.log('Sending scheduled trigger notification email...'); await transporter.sendMail(mailOptions); console.log('Scheduled trigger notification email sent successfully'); return { success: true, message: 'Scheduled trigger notification sent' }; } catch (error) { console.error('Error sending scheduled trigger notification:', error); return { success: false, error: error.message }; } } /** * Send scan failure notification email * @param {Object} env - Environment variables * @param {string} errorMessage - The error message from the failed scan * @returns {Object} - Result object with success status and message/error */ export async function sendScanFailureNotification(env, errorMessage) { try { // Check if DIGEST_TO is configured if (!env.DIGEST_TO) { console.log('DIGEST_TO not configured, skipping failure notification'); return { success: false, error: 'DIGEST_TO not configured' }; } // Check SMTP configuration const smtpCheck = checkSmtpConfiguration(env); if (!smtpCheck.isConfigured) { console.log(`SMTP not configured, skipping failure notification. Missing: ${smtpCheck.missingVars.join(', ')}`); return { success: false, error: 'SMTP not configured', missingVars: smtpCheck.missingVars }; } // Send failure notification email directly (no jobs needed) console.log('Sending scan failure notification email...'); const emailResult = await sendDigestEmail(env.DIGEST_TO, [], env, { source: 'scan_failed', error: errorMessage }); if (emailResult.success) { console.log('Scan failure notification sent successfully'); return { success: true, message: 'Scan failure notification sent' }; } else { console.log(`Failed to send scan failure notification: ${emailResult.error}`); return { success: false, error: emailResult.error }; } } catch (error) { console.error('Error sending scan failure notification:', error); return { success: false, error: error.message }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/adamd9/mcp-jobsearch'

If you have feedback or need assistance with the MCP directory API, please join our Discord server