@alexdiazdecerio/fastmail-mcp-server
Version:
š Professional MCP Server for Fastmail email management using JMAP API - Integrates seamlessly with Claude Desktop and other AI assistants
342 lines (335 loc) ⢠15.7 kB
JavaScript
export class EmailAnalyticsEngine {
fastmailClient;
constructor(fastmailClient) {
this.fastmailClient = fastmailClient;
}
/**
* Generate comprehensive email analytics for a given period
*/
async generateAnalytics(options = {}) {
const { startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days ago
endDate = new Date().toISOString(), maxEmails = 1000, includeContent = true } = options;
console.error(`Generating analytics from ${startDate} to ${endDate}`);
// Get all emails in the period
const emails = await this.getEmailsInPeriod(startDate, endDate, maxEmails);
console.error(`Analyzing ${emails.length} emails...`);
// Generate analytics
const analytics = {
emailVolume: this.calculateVolumeAnalytics(emails, startDate, endDate),
topSenders: this.calculateSenderAnalytics(emails),
activityPatterns: this.calculateTemporalAnalytics(emails),
folderUsage: await this.calculateFolderAnalytics(emails),
contentInsights: includeContent ? this.calculateContentAnalytics(emails) : {
averageEmailLength: 0,
totalAttachments: 0,
commonKeywords: [],
subjectAnalysis: []
},
responseMetrics: this.calculateResponseMetrics(emails)
};
return analytics;
}
/**
* Get volume analytics for the specified period
*/
async getVolumeAnalytics(days = 30) {
const endDate = new Date().toISOString();
const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
const emails = await this.getEmailsInPeriod(startDate, endDate, 1000);
return this.calculateVolumeAnalytics(emails, startDate, endDate);
}
/**
* Get top senders analytics
*/
async getTopSenders(limit = 10, days = 30) {
const endDate = new Date().toISOString();
const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
const emails = await this.getEmailsInPeriod(startDate, endDate, 1000);
return this.calculateSenderAnalytics(emails).slice(0, limit);
}
/**
* Get activity patterns for visualization
*/
async getActivityPatterns(days = 30) {
const endDate = new Date().toISOString();
const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
const emails = await this.getEmailsInPeriod(startDate, endDate, 1000);
return this.calculateTemporalAnalytics(emails);
}
/**
* Generate a comprehensive email report
*/
async generateEmailReport(days = 30) {
const analytics = await this.generateAnalytics({
startDate: new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(),
endDate: new Date().toISOString(),
maxEmails: 1000,
includeContent: true
});
return this.formatAnalyticsReport(analytics, days);
}
// Private helper methods
async getEmailsInPeriod(startDate, endDate, maxEmails) {
const emails = [];
const start = new Date(startDate);
const end = new Date(endDate);
try {
// Get emails from inbox and sent folders
const inboxEmails = await this.fastmailClient.getEmails({
limit: Math.floor(maxEmails / 2),
filter: {
after: start.toISOString()
}
});
// For sent emails, we'll try to find a sent folder
// This is a simplified approach - in a real implementation you'd get the Sent mailbox ID
const sentEmails = await this.fastmailClient.getEmails({
limit: Math.floor(maxEmails / 2),
filter: {
after: start.toISOString()
}
});
// Combine all emails
const allEmails = [...inboxEmails.emails, ...sentEmails.emails];
// Filter by date range
return allEmails.filter(email => {
const receivedDate = new Date(email.receivedAt);
return receivedDate >= start && receivedDate <= end;
});
}
catch (error) {
console.error('Error fetching emails for analytics:', error);
return [];
}
}
calculateVolumeAnalytics(emails, startDate, endDate) {
const userEmail = process.env.FASTMAIL_EMAIL?.toLowerCase();
const sentEmails = emails.filter(email => email.from?.email?.toLowerCase() === userEmail);
const receivedEmails = emails.filter(email => email.from?.email?.toLowerCase() !== userEmail);
return {
totalEmails: emails.length,
sentEmails: sentEmails.length,
receivedEmails: receivedEmails.length,
periodStart: startDate,
periodEnd: endDate
};
}
calculateSenderAnalytics(emails) {
const userEmail = process.env.FASTMAIL_EMAIL?.toLowerCase();
const senderCounts = new Map();
// Only count received emails (not sent)
const receivedEmails = emails.filter(email => email.from?.email?.toLowerCase() !== userEmail);
receivedEmails.forEach(email => {
if (email.from?.email) {
const senderEmail = email.from.email.toLowerCase();
const existing = senderCounts.get(senderEmail);
if (existing) {
existing.count++;
}
else {
senderCounts.set(senderEmail, {
name: email.from.name,
count: 1
});
}
}
});
const totalReceived = receivedEmails.length;
const topSenders = Array.from(senderCounts.entries())
.map(([email, data]) => ({
email,
name: data.name,
count: data.count,
percentage: Math.round((data.count / totalReceived) * 100 * 100) / 100
}))
.sort((a, b) => b.count - a.count);
return topSenders;
}
calculateTemporalAnalytics(emails) {
const hourCounts = new Array(24).fill(0);
const dayCounts = {};
const monthCounts = {};
emails.forEach(email => {
const date = new Date(email.receivedAt);
// Hour analysis
hourCounts[date.getHours()]++;
// Day analysis
const dayKey = date.toISOString().split('T')[0];
dayCounts[dayKey] = (dayCounts[dayKey] || 0) + 1;
// Month analysis
const monthKey = date.toISOString().substring(0, 7); // YYYY-MM
monthCounts[monthKey] = (monthCounts[monthKey] || 0) + 1;
});
return {
byHour: hourCounts.map((count, hour) => ({ hour, count })),
byDay: Object.entries(dayCounts)
.map(([day, count]) => ({ day, count }))
.sort((a, b) => a.day.localeCompare(b.day)),
byMonth: Object.entries(monthCounts)
.map(([month, count]) => ({ month, count }))
.sort((a, b) => a.month.localeCompare(b.month))
};
}
async calculateFolderAnalytics(emails) {
const folderCounts = new Map();
try {
// Get all mailboxes to map IDs to names
const mailboxes = await this.fastmailClient.getMailboxes();
const mailboxMap = new Map();
mailboxes?.forEach((mailbox) => {
mailboxMap.set(mailbox.id, mailbox.name);
});
// Count emails per folder
emails.forEach(email => {
if (email.mailboxIds) {
Object.keys(email.mailboxIds).forEach(mailboxId => {
const folderName = mailboxMap.get(mailboxId) || mailboxId;
folderCounts.set(folderName, (folderCounts.get(folderName) || 0) + 1);
});
}
});
const totalEmails = emails.length;
return Array.from(folderCounts.entries())
.map(([folderName, emailCount]) => ({
folderName,
emailCount,
percentage: Math.round((emailCount / totalEmails) * 100 * 100) / 100
}))
.sort((a, b) => b.emailCount - a.emailCount);
}
catch (error) {
console.error('Error calculating folder analytics:', error);
return [];
}
}
calculateContentAnalytics(emails) {
let totalLength = 0;
let totalAttachments = 0;
const wordFrequency = new Map();
const subjectPatterns = new Map();
emails.forEach(email => {
// Calculate email length
if (email.preview) {
totalLength += email.preview.length;
}
// Count attachments
if (email.hasAttachment) {
totalAttachments++;
}
// Analyze subject for patterns
if (email.subject) {
const subject = email.subject.toLowerCase();
// Check for common patterns
if (subject.includes('re:')) {
subjectPatterns.set('Replies (Re:)', (subjectPatterns.get('Replies (Re:)') || 0) + 1);
}
if (subject.includes('fwd:') || subject.includes('fw:')) {
subjectPatterns.set('Forwards (Fwd:)', (subjectPatterns.get('Forwards (Fwd:)') || 0) + 1);
}
if (subject.includes('newsletter') || subject.includes('unsubscribe')) {
subjectPatterns.set('Newsletters', (subjectPatterns.get('Newsletters') || 0) + 1);
}
if (subject.includes('invoice') || subject.includes('payment') || subject.includes('bill')) {
subjectPatterns.set('Financial', (subjectPatterns.get('Financial') || 0) + 1);
}
if (subject.includes('meeting') || subject.includes('calendar')) {
subjectPatterns.set('Meetings/Calendar', (subjectPatterns.get('Meetings/Calendar') || 0) + 1);
}
// Extract keywords (simple implementation)
const words = subject.split(/\s+/).filter((word) => word.length > 3 &&
!['from', 'with', 'your', 'this', 'that', 'have', 'will', 'been', 'were'].includes(word));
words.forEach((word) => {
const cleanWord = word.replace(/[^\w]/g, '').toLowerCase();
if (cleanWord.length > 3) {
wordFrequency.set(cleanWord, (wordFrequency.get(cleanWord) || 0) + 1);
}
});
}
});
// Get top keywords
const topKeywords = Array.from(wordFrequency.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([word, frequency]) => ({ word, frequency }));
// Get subject patterns
const topSubjectPatterns = Array.from(subjectPatterns.entries())
.sort((a, b) => b[1] - a[1])
.map(([pattern, count]) => ({ pattern, count }));
return {
averageEmailLength: emails.length > 0 ? Math.round(totalLength / emails.length) : 0,
totalAttachments,
commonKeywords: topKeywords,
subjectAnalysis: topSubjectPatterns
};
}
calculateResponseMetrics(emails) {
const userEmail = process.env.FASTMAIL_EMAIL?.toLowerCase();
// Count unread emails
const unreadEmails = emails.filter(email => !email.isRead && email.from?.email?.toLowerCase() !== userEmail);
// Find oldest unread email
const oldestUnread = unreadEmails.length > 0
? unreadEmails.reduce((oldest, email) => new Date(email.receivedAt) < new Date(oldest.receivedAt) ? email : oldest)
: null;
// Calculate average response time (simplified - would need thread analysis for accuracy)
const receivedEmails = emails.filter(email => email.from?.email?.toLowerCase() !== userEmail);
const totalEmails = receivedEmails.length;
const unreadCount = unreadEmails.length;
return {
averageResponseTime: 0, // Would need thread analysis to calculate properly
unreadCount,
unreadPercentage: totalEmails > 0 ? Math.round((unreadCount / totalEmails) * 100 * 100) / 100 : 0,
oldestUnread: oldestUnread?.receivedAt
};
}
formatAnalyticsReport(analytics, days) {
const report = `
š EMAIL ANALYTICS REPORT (Last ${days} days)
${'='.repeat(50)}
š VOLUME SUMMARY
⢠Total Emails: ${analytics.emailVolume.totalEmails}
⢠Sent: ${analytics.emailVolume.sentEmails}
⢠Received: ${analytics.emailVolume.receivedEmails}
⢠Daily Average: ${Math.round(analytics.emailVolume.totalEmails / days)} emails/day
š„ TOP SENDERS
${analytics.topSenders.slice(0, 5).map((sender, i) => `${i + 1}. ${sender.name || sender.email} (${sender.count} emails, ${sender.percentage}%)`).join('\n')}
ā° PEAK ACTIVITY HOURS
${analytics.activityPatterns.byHour
.map((hour, i) => ({ hour: i, count: hour.count }))
.sort((a, b) => b.count - a.count)
.slice(0, 3)
.map((h, i) => `${i + 1}. ${h.hour}:00 - ${h.hour + 1}:00 (${h.count} emails)`)
.join('\n')}
š FOLDER USAGE
${analytics.folderUsage.slice(0, 5).map((folder, i) => `${i + 1}. ${folder.folderName}: ${folder.emailCount} emails (${folder.percentage}%)`).join('\n')}
š CONTENT INSIGHTS
⢠Average Email Length: ${analytics.contentInsights.averageEmailLength} characters
⢠Total Attachments: ${analytics.contentInsights.totalAttachments}
⢠Top Keywords: ${analytics.contentInsights.commonKeywords.slice(0, 3).map(k => k.word).join(', ')}
ā” PRODUCTIVITY METRICS
⢠Unread Emails: ${analytics.responseMetrics.unreadCount}
⢠Unread Percentage: ${analytics.responseMetrics.unreadPercentage}%
${analytics.responseMetrics.oldestUnread ? `⢠Oldest Unread: ${new Date(analytics.responseMetrics.oldestUnread).toLocaleDateString()}` : ''}
šÆ RECOMMENDATIONS
${this.generateRecommendations(analytics)}
`;
return report.trim();
}
generateRecommendations(analytics) {
const recommendations = [];
if (analytics.responseMetrics.unreadPercentage > 20) {
recommendations.push("⢠Consider setting aside specific times for email processing");
}
if (analytics.topSenders.length > 0 && analytics.topSenders[0].percentage > 30) {
recommendations.push(`⢠${analytics.topSenders[0].name || analytics.topSenders[0].email} sends 30%+ of your emails - consider filters`);
}
if (analytics.contentInsights.totalAttachments > analytics.emailVolume.totalEmails * 0.5) {
recommendations.push("⢠High attachment volume - consider cloud storage integration");
}
const peakHour = analytics.activityPatterns.byHour.reduce((max, hour, i) => hour.count > analytics.activityPatterns.byHour[max].count ? i : max, 0);
if (peakHour < 9 || peakHour > 17) {
recommendations.push("⢠Most emails arrive outside business hours - consider notification schedules");
}
return recommendations.length > 0 ? recommendations.join('\n') : "⢠Your email management looks healthy!";
}
}
//# sourceMappingURL=email-analytics.js.map