import fetch from 'node-fetch';
export class FastMailClient {
constructor(apiToken, email, sendAs, aliasDomain, jmapUrl) {
this.apiToken = apiToken;
this.email = email;
this.sendAs = sendAs;
this.aliasDomain = aliasDomain;
this.jmapUrl = jmapUrl;
this.session = null;
this.accountId = null;
this.emailSendCache = new Map(); // Track recent email sends to prevent duplicates
this.duplicatePreventionWindow = 5 * 60 * 1000; // 5 minutes in milliseconds
}
async authenticate() {
const response = await fetch(this.jmapUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiToken}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);
}
this.session = await response.json();
this.accountId = this.session.primaryAccounts['urn:ietf:params:jmap:mail'];
if (!this.accountId) {
throw new Error('No mail account found in session');
}
return this.session;
}
async makeJmapRequest(methodCalls) {
if (!this.session) {
await this.authenticate();
}
const requestBody = {
using: [
'urn:ietf:params:jmap:core',
'urn:ietf:params:jmap:mail',
'urn:ietf:params:jmap:submission'
],
methodCalls: methodCalls
};
const response = await fetch(this.session.apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`JMAP request failed: ${response.status} ${response.statusText}`);
}
return await response.json();
}
async getMailboxes() {
const methodCalls = [
['Mailbox/get', {
accountId: this.accountId,
}, 'mailboxes']
];
const response = await this.makeJmapRequest(methodCalls);
return response.methodResponses[0][1].list;
}
async getEmails(mailboxId, limit = 50, position = 0) {
const methodCalls = [
['Email/query', {
accountId: this.accountId,
filter: mailboxId ? { inMailbox: mailboxId } : null,
sort: [{ property: 'receivedAt', isAscending: false }],
position: position,
limit: limit
}, 'query'],
['Email/get', {
accountId: this.accountId,
'#ids': {
resultOf: 'query',
name: 'Email/query',
path: '/ids'
},
properties: ['id', 'subject', 'from', 'to', 'receivedAt', 'size', 'preview']
}, 'emails']
];
const response = await this.makeJmapRequest(methodCalls);
const emails = response.methodResponses[1][1].list;
const queryResult = response.methodResponses[0][1];
return {
emails: emails,
total: queryResult.total,
position: queryResult.position,
hasMore: (queryResult.position + emails.length) < queryResult.total,
queryState: queryResult.queryState
};
}
async getAllEmails(mailboxId, batchSize = 50, maxTotal = 1000) {
const allEmails = [];
let position = 0;
let hasMore = true;
console.log(`π¬ Getting emails from mailbox ${mailboxId || 'all'} with pagination...`);
while (hasMore && allEmails.length < maxTotal) {
const remainingLimit = Math.min(batchSize, maxTotal - allEmails.length);
console.log(`π€ Fetching batch at position ${position}, limit ${remainingLimit}`);
const result = await this.getEmails(mailboxId, remainingLimit, position);
if (result.emails && result.emails.length > 0) {
allEmails.push(...result.emails);
console.log(`π₯ Retrieved ${result.emails.length} emails, total so far: ${allEmails.length}`);
position += result.emails.length;
hasMore = result.hasMore;
// Prevent infinite loops - if we didn't get expected results
if (result.emails.length === 0) {
console.log('β οΈ No more emails in batch, stopping pagination');
break;
}
} else {
console.log('π No more emails to retrieve');
break;
}
// Small delay to prevent overwhelming the server
if (hasMore && allEmails.length < maxTotal) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
console.log(`β
Retrieved ${allEmails.length} total emails`);
return allEmails;
}
async getEmailById(emailId) {
const methodCalls = [
['Email/get', {
accountId: this.accountId,
ids: [emailId],
properties: ['id', 'subject', 'from', 'to', 'cc', 'bcc', 'receivedAt', 'size', 'preview', 'bodyStructure', 'bodyValues']
}, 'email']
];
const response = await this.makeJmapRequest(methodCalls);
return response.methodResponses[0][1].list[0];
}
generateEmailKey(to, subject, textBody) {
// Create a unique key for this email to detect duplicates
const recipients = Array.isArray(to) ? to.sort().join(',') : to;
const contentHash = Buffer.from(`${recipients}::${subject}::${textBody.substring(0, 200)}`).toString('base64');
return contentHash;
}
cleanExpiredCacheEntries() {
const now = Date.now();
for (const [key, timestamp] of this.emailSendCache.entries()) {
if (now - timestamp > this.duplicatePreventionWindow) {
this.emailSendCache.delete(key);
}
}
}
async sendEmail(to, subject, textBody, htmlBody = null, fromAlias = null) {
console.log(`π§ Sending email to: ${to}`);
console.log(`π Subject: ${subject}`);
// Generate a unique key for this email to prevent duplicates
const emailKey = this.generateEmailKey(to, subject, textBody);
const now = Date.now();
// Clean expired cache entries
this.cleanExpiredCacheEntries();
// Check if we've sent this exact email recently
if (this.emailSendCache.has(emailKey)) {
const lastSentTime = this.emailSendCache.get(emailKey);
const timeSinceLastSend = now - lastSentTime;
if (timeSinceLastSend < this.duplicatePreventionWindow) {
const minutesAgo = Math.round(timeSinceLastSend / 1000 / 60);
console.warn(`β οΈ DUPLICATE PREVENTION: This email was already sent ${minutesAgo} minutes ago`);
console.warn(`π§ To: ${to}, Subject: ${subject}`);
throw new Error(`Duplicate email prevented: This exact email was sent ${minutesAgo} minutes ago. Wait ${Math.ceil((this.duplicatePreventionWindow - timeSinceLastSend) / 1000 / 60)} more minutes to send again.`);
}
}
// Record this email attempt
this.emailSendCache.set(emailKey, now);
console.log(`π Email recorded in duplicate prevention cache (key: ${emailKey.substring(0, 16)}...)`);
// Now that blob upload works, try the full JMAP implementation
console.log('π Attempting JMAP email sending with blobs...');
try {
const result = await this.sendEmailJMAP(to, subject, textBody, htmlBody, fromAlias);
console.log('β
JMAP send successful');
console.log(`π Email cache size: ${this.emailSendCache.size} recent sends`);
return result;
} catch (jmapError) {
// Remove from cache if sending failed
this.emailSendCache.delete(emailKey);
console.error('β JMAP sending failed, removed from cache:', jmapError.message);
throw new Error(`Email sending failed - JMAP: ${jmapError.message}`);
}
}
async sendEmailSimpleJMAP(to, subject, textBody, htmlBody = null, fromAlias = null) {
const fromEmail = fromAlias ? `${fromAlias}@${this.aliasDomain}` : this.sendAs;
// Ensure we're authenticated
if (!this.session) {
await this.authenticate();
}
// Get the Sent mailbox
const mailboxes = await this.getMailboxes();
const sentMailbox = mailboxes.find(mb => mb.role === 'sent' || mb.name === 'Sent');
if (!sentMailbox) {
throw new Error('No Sent mailbox found');
}
console.log(`π¬ Creating email in Sent folder (${sentMailbox.name})`);
// Create email directly in Sent folder (simple approach)
const email = {
from: [{ email: fromEmail }],
to: Array.isArray(to) ? to.map(email => ({ email })) : [{ email: to }],
subject: subject,
mailboxIds: { [sentMailbox.id]: true },
receivedAt: new Date().toISOString(),
bodyStructure: {
type: 'text/plain',
bodyValue: 'textPart'
},
bodyValues: {
textPart: {
value: textBody,
charset: 'utf-8'
}
}
};
if (htmlBody) {
email.bodyStructure = {
type: 'multipart/alternative',
subParts: [
{
type: 'text/plain',
bodyValue: 'textPart'
},
{
type: 'text/html',
bodyValue: 'htmlPart'
}
]
};
email.bodyValues.htmlPart = {
value: htmlBody,
charset: 'utf-8'
};
}
const methodCalls = [
['Email/set', {
accountId: this.accountId,
create: {
'email1': email
}
}, 'createEmail']
];
const response = await this.makeJmapRequest(methodCalls);
if (response.methodResponses[0][1].created) {
const emailId = response.methodResponses[0][1].created['email1'].id;
console.log(`β
Email created in Sent folder with ID: ${emailId}`);
// Now try to actually send it via a simple web request to the recipient
// This is a workaround since we can't use SMTP with current credentials
console.log('π€ Note: Email saved to Sent folder. For actual delivery, manual sending may be required.');
return {
success: true,
method: 'SimpleJMAP',
emailId: emailId,
note: 'Email created in Sent folder. Actual delivery depends on FastMail processing.'
};
} else {
throw new Error(`Email creation failed: ${JSON.stringify(response.methodResponses[0][1])}`);
}
}
async sendEmailJMAP(to, subject, textBody, htmlBody = null, fromAlias = null) {
const fromEmail = fromAlias ? `${fromAlias}@${this.aliasDomain}` : this.sendAs;
// Ensure we're authenticated
if (!this.session) {
await this.authenticate();
}
// Get mailboxes
const mailboxes = await this.getMailboxes();
const sentMailbox = mailboxes.find(mb => mb.role === 'sent' || mb.name === 'Sent');
if (!sentMailbox) {
throw new Error('No Sent mailbox found for JMAP sending');
}
// Step 1: Upload content as blobs
console.log('π€ Uploading email content as blobs...');
const textBlobId = await this.uploadBlob(textBody);
let htmlBlobId = null;
if (htmlBody) {
htmlBlobId = await this.uploadBlob(htmlBody);
}
// Step 2: Create email with proper blob structure
console.log('π Creating email object...');
const email = {
from: [{ email: fromEmail }],
to: Array.isArray(to) ? to.map(email => ({ email })) : [{ email: to }],
subject: subject,
mailboxIds: { [sentMailbox.id]: true },
bodyStructure: htmlBody ? {
type: 'multipart/alternative',
subParts: [
{
type: 'text/plain',
blobId: textBlobId,
charset: 'utf-8',
disposition: 'inline'
},
{
type: 'text/html',
blobId: htmlBlobId,
charset: 'utf-8'
}
]
} : {
type: 'text/plain',
blobId: textBlobId,
charset: 'utf-8',
disposition: 'inline'
}
};
const emailResponse = await this.makeJmapRequestWithCapabilities(['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], [
['Email/set', {
accountId: this.accountId,
create: {
'email1': email
}
}, 'email']
]);
if (!emailResponse.methodResponses[0][1].created) {
throw new Error(`Email creation failed: ${JSON.stringify(emailResponse.methodResponses[0][1])}`);
}
const emailId = emailResponse.methodResponses[0][1].created['email1'].id;
console.log('β
Email created with ID:', emailId);
// Step 3: Submit email for sending
console.log('π¬ Submitting email for sending...');
// Get the correct identity for the sending email
let identityId = '154447007'; // Default to clark@clarkeverson.com identity
if (fromEmail === 'clark@everson.dev') {
identityId = '154446543';
} else if (fromEmail.endsWith('@clarkeverson.com')) {
// Use the catch-all identity for other @clarkeverson.com addresses
identityId = '154446999';
}
console.log(`π Using identity ID: ${identityId} for ${fromEmail}`);
const submissionResponse = await this.makeJmapRequestWithCapabilities(['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:submission'], [
['EmailSubmission/set', {
accountId: this.accountId,
create: {
'submission1': {
emailId: emailId,
identityId: identityId,
envelope: {
mailFrom: { email: fromEmail },
rcptTo: Array.isArray(to) ? to.map(email => ({ email })) : [{ email: to }]
}
}
}
}, 'submission']
]);
if (!submissionResponse.methodResponses[0][1].created) {
throw new Error(`Email submission failed: ${JSON.stringify(submissionResponse.methodResponses[0][1])}`);
}
console.log('β
Email submitted successfully');
return {
success: true,
method: 'JMAP',
emailId: emailId,
submissionId: submissionResponse.methodResponses[0][1].created['submission1'].id
};
}
async uploadBlob(content) {
if (!this.session || !this.session.uploadUrl) {
throw new Error('No upload URL available in session');
}
// Replace {accountId} in the upload URL template
const uploadUrl = this.session.uploadUrl.replace('{accountId}', this.accountId);
console.log(`π€ Uploading blob to: ${uploadUrl}`);
const response = await fetch(uploadUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiToken}`,
'Content-Type': 'application/octet-stream',
},
body: content
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Blob upload failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const result = await response.json();
console.log('π¦ Blob upload response:', result);
if (!result.blobId) {
throw new Error('No blobId returned from upload');
}
return result.blobId;
}
async makeJmapRequestWithCapabilities(capabilities, methodCalls) {
if (!this.session) {
await this.authenticate();
}
const requestBody = {
using: capabilities,
methodCalls: methodCalls
};
const response = await fetch(this.session.apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`JMAP request failed: ${response.status} ${response.statusText}`);
}
return await response.json();
}
async sendEmailSMTP(to, subject, textBody, htmlBody = null, fromAlias = null) {
try {
const nodemailer = (await import('nodemailer')).default;
const fromEmail = fromAlias ? `${fromAlias}@${this.aliasDomain}` : this.sendAs;
console.log(`π SMTP Auth - User: ${this.sendAs}`);
console.log(`π§ From: ${fromEmail}`);
console.log(`π¬ To: ${Array.isArray(to) ? to.join(', ') : to}`);
// FastMail SMTP configuration
const transporter = nodemailer.createTransport({
host: 'smtp.fastmail.com',
port: 587,
secure: false, // TLS
requireTLS: true,
auth: {
user: this.sendAs, // Try using sendAs email instead of this.email
pass: this.apiToken // Use app password for SMTP
},
debug: false, // Set to true for detailed logs
logger: false
});
// Verify connection first
console.log('π Verifying SMTP connection...');
await transporter.verify();
console.log('β
SMTP connection verified');
const mailOptions = {
from: fromEmail,
to: Array.isArray(to) ? to.join(', ') : to,
subject: subject,
text: textBody,
html: htmlBody || null,
envelope: {
from: fromEmail,
to: Array.isArray(to) ? to : [to]
}
};
console.log('π Sending email via SMTP...');
const info = await transporter.sendMail(mailOptions);
console.log('β
SMTP send successful!');
console.log(`π Message ID: ${info.messageId}`);
console.log(`π Response: ${info.response}`);
// Close the transporter
transporter.close();
return {
success: true,
method: 'SMTP',
messageId: info.messageId,
response: info.response,
accepted: info.accepted,
rejected: info.rejected
};
} catch (error) {
console.error('β SMTP send failed with error:', error);
console.error('β Error code:', error.code);
console.error('β Error command:', error.command);
throw new Error(`SMTP sending failed: ${error.message} (${error.code || 'unknown'})`);
}
}
async searchEmails(query, limit = 50, position = 0) {
const methodCalls = [
['Email/query', {
accountId: this.accountId,
filter: {
text: query
},
sort: [{ property: 'receivedAt', isAscending: false }],
position: position,
limit: limit
}, 'query'],
['Email/get', {
accountId: this.accountId,
'#ids': {
resultOf: 'query',
name: 'Email/query',
path: '/ids'
},
properties: ['id', 'subject', 'from', 'to', 'receivedAt', 'size', 'preview']
}, 'emails']
];
const response = await this.makeJmapRequest(methodCalls);
const emails = response.methodResponses[1][1].list;
const queryResult = response.methodResponses[0][1];
return {
emails: emails,
total: queryResult.total,
position: queryResult.position,
hasMore: (queryResult.position + emails.length) < queryResult.total,
queryState: queryResult.queryState
};
}
async searchAllEmails(query, batchSize = 50, maxTotal = 500) {
const allEmails = [];
let position = 0;
let hasMore = true;
console.log(`π Searching emails for "${query}" with pagination...`);
while (hasMore && allEmails.length < maxTotal) {
const remainingLimit = Math.min(batchSize, maxTotal - allEmails.length);
console.log(`π€ Searching batch at position ${position}, limit ${remainingLimit}`);
const result = await this.searchEmails(query, remainingLimit, position);
if (result.emails && result.emails.length > 0) {
allEmails.push(...result.emails);
console.log(`π₯ Found ${result.emails.length} emails, total so far: ${allEmails.length}`);
position += result.emails.length;
hasMore = result.hasMore;
if (result.emails.length === 0) {
console.log('β οΈ No more emails in search batch, stopping pagination');
break;
}
} else {
console.log('π No more search results');
break;
}
// Small delay to prevent overwhelming the server
if (hasMore && allEmails.length < maxTotal) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
console.log(`β
Found ${allEmails.length} total emails matching "${query}"`);
return allEmails;
}
async createMailbox(name, parentId = null, role = null) {
const mailboxData = {
name: name,
sortOrder: 10
};
if (parentId) {
mailboxData.parentId = parentId;
}
if (role) {
mailboxData.role = role;
}
const methodCalls = [
['Mailbox/set', {
accountId: this.accountId,
create: {
'new-mailbox': mailboxData
}
}, 'createMailbox']
];
const response = await this.makeJmapRequest(methodCalls);
if (response.methodResponses[0][1].created) {
return response.methodResponses[0][1].created['new-mailbox'];
} else {
throw new Error(`Failed to create mailbox: ${JSON.stringify(response.methodResponses[0][1])}`);
}
}
/**
* Create a hierarchical mailbox structure from path like "Financial/Banking"
* @param {string} hierarchicalPath - Path like "Financial/Banking"
* @returns {Object} - The final mailbox object
*/
async createHierarchicalMailbox(hierarchicalPath) {
const parts = hierarchicalPath.split('/');
const existingMailboxes = await this.getMailboxes();
let currentParentId = null;
let currentPath = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
currentPath = currentPath ? `${currentPath}/${part}` : part;
// Check if this level already exists
let existing = null;
if (currentParentId) {
// Look for child with this name under the current parent
existing = existingMailboxes.find(mb =>
mb.name === part && mb.parentId === currentParentId
);
} else {
// Look for top-level mailbox with this name
existing = existingMailboxes.find(mb =>
mb.name === part && !mb.parentId
);
}
if (existing) {
currentParentId = existing.id;
} else {
// Create this level
console.log(`π Creating hierarchical folder: ${currentPath}`);
const newMailbox = await this.createMailbox(part, currentParentId);
currentParentId = newMailbox.id;
// Add to our tracking
existingMailboxes.push({
id: newMailbox.id,
name: part,
parentId: currentParentId === newMailbox.id ? null : currentParentId
});
}
}
// Return the final mailbox
return existingMailboxes.find(mb => mb.id === currentParentId);
}
async moveEmailsToMailbox(emailIds, mailboxId) {
const methodCalls = [
['Email/set', {
accountId: this.accountId,
update: {}
}, 'moveEmails']
];
// Build update object for each email
emailIds.forEach(emailId => {
methodCalls[0][1].update[emailId] = {
mailboxIds: { [mailboxId]: true }
};
});
const response = await this.makeJmapRequest(methodCalls);
if (response.methodResponses[0][1].updated) {
return response.methodResponses[0][1].updated;
} else {
throw new Error(`Failed to move emails: ${JSON.stringify(response.methodResponses[0][1])}`);
}
}
/**
* Assign emails to multiple mailboxes (multi-label support)
* @param {Array} emailIds - Array of email IDs
* @param {Array} mailboxIds - Array of mailbox IDs to assign
* @param {boolean} preserveExisting - Whether to keep existing mailbox assignments
* @returns {Object} - Update result
*/
async assignEmailsToMailboxes(emailIds, mailboxIds, preserveExisting = false) {
if (!Array.isArray(emailIds) || !Array.isArray(mailboxIds)) {
throw new Error('emailIds and mailboxIds must be arrays');
}
if (emailIds.length === 0 || mailboxIds.length === 0) {
throw new Error('emailIds and mailboxIds cannot be empty');
}
// Get current mailbox assignments if preserving existing
let currentAssignments = {};
if (preserveExisting) {
const emailsResponse = await this.makeJmapRequest([
['Email/get', {
accountId: this.accountId,
ids: emailIds,
properties: ['id', 'mailboxIds']
}, 'getCurrentAssignments']
]);
const emails = emailsResponse.methodResponses[0][1].list;
emails.forEach(email => {
currentAssignments[email.id] = email.mailboxIds || {};
});
}
const methodCalls = [
['Email/set', {
accountId: this.accountId,
update: {}
}, 'assignToMultipleMailboxes']
];
// Build update object for each email
emailIds.forEach(emailId => {
const newMailboxIds = {};
// Add existing mailboxes if preserving
if (preserveExisting && currentAssignments[emailId]) {
Object.assign(newMailboxIds, currentAssignments[emailId]);
}
// Add new mailboxes
mailboxIds.forEach(mailboxId => {
newMailboxIds[mailboxId] = true;
});
methodCalls[0][1].update[emailId] = {
mailboxIds: newMailboxIds
};
});
const response = await this.makeJmapRequest(methodCalls);
if (response.methodResponses[0][1].updated) {
return {
updated: response.methodResponses[0][1].updated,
assignedToMailboxes: mailboxIds.length,
emailsProcessed: emailIds.length
};
} else {
throw new Error(`Failed to assign emails to mailboxes: ${JSON.stringify(response.methodResponses[0][1])}`);
}
}
/**
* Find or create multiple hierarchical mailboxes by path
* @param {Array} mailboxNames - Array of hierarchical mailbox paths like ["Financial/Banking", "Commerce/Orders"]
* @returns {Array} - Array of mailbox objects with IDs
*/
async findOrCreateMultipleMailboxes(mailboxNames) {
if (!Array.isArray(mailboxNames)) {
throw new Error('mailboxNames must be an array');
}
const results = [];
// Create missing hierarchical mailboxes
for (const name of mailboxNames) {
try {
// For hierarchical paths, we need to find the final mailbox
const existingMailboxes = await this.getMailboxes();
let existing = this.findHierarchicalMailbox(existingMailboxes, name);
if (existing) {
results.push({ name, mailbox: existing, created: false });
} else {
const newMailbox = await this.createHierarchicalMailbox(name);
results.push({ name, mailbox: newMailbox, created: true });
}
} catch (error) {
console.error(`Failed to create hierarchical mailbox ${name}:`, error);
// Add error result
results.push({ name, mailbox: null, created: false, error: error.message });
}
}
return results;
}
/**
* Find a hierarchical mailbox by path like "Financial/Banking"
* @param {Array} mailboxes - Array of all mailboxes
* @param {string} hierarchicalPath - Path like "Financial/Banking"
* @returns {Object|null} - The mailbox object or null if not found
*/
findHierarchicalMailbox(mailboxes, hierarchicalPath) {
const parts = hierarchicalPath.split('/');
let currentParentId = null;
let currentMailbox = null;
for (const part of parts) {
if (currentParentId === null) {
// Look for top-level mailbox
currentMailbox = mailboxes.find(mb => mb.name === part && !mb.parentId);
} else {
// Look for child mailbox
currentMailbox = mailboxes.find(mb => mb.name === part && mb.parentId === currentParentId);
}
if (!currentMailbox) {
return null; // Path doesn't exist
}
currentParentId = currentMailbox.id;
}
return currentMailbox;
}
async findMailboxByName(name) {
const mailboxes = await this.getMailboxes();
return mailboxes.find(mailbox => mailbox.name === name);
}
async findOrCreateMailbox(name, parentId = null) {
// First try to find existing mailbox
const existing = await this.findMailboxByName(name);
if (existing) {
return existing;
}
// Create new mailbox if it doesn't exist
return await this.createMailbox(name, parentId);
}
/**
* Delete a mailbox by ID
* @param {string} mailboxId - The ID of the mailbox to delete
* @returns {Object} - The deletion result
*/
async deleteMailbox(mailboxId) {
const methodCalls = [
['Mailbox/set', {
accountId: this.accountId,
destroy: [mailboxId]
}, 'deleteMailbox']
];
const response = await this.makeJmapRequest(methodCalls);
if (response.methodResponses[0][1].destroyed && response.methodResponses[0][1].destroyed.includes(mailboxId)) {
return { success: true, deletedId: mailboxId };
} else {
throw new Error(`Failed to delete mailbox: ${JSON.stringify(response.methodResponses[0][1])}`);
}
}
/**
* Get current mailbox assignments for emails
* @param {Array} emailIds - Array of email IDs
* @returns {Object} - Map of email ID to mailbox IDs
*/
async getEmailMailboxAssignments(emailIds) {
if (!Array.isArray(emailIds) || emailIds.length === 0) {
return {};
}
const response = await this.makeJmapRequest([
['Email/get', {
accountId: this.accountId,
ids: emailIds,
properties: ['id', 'mailboxIds']
}, 'getAssignments']
]);
const assignments = {};
const emails = response.methodResponses[0][1].list;
emails.forEach(email => {
assignments[email.id] = email.mailboxIds || {};
});
return assignments;
}
}