Skip to main content

Message Operations

Message operations allow you to list, fetch, move, delete, and update email messages programmatically. These operations work consistently across IMAP, Gmail API, and Microsoft Graph backends.

Listing Messages

Basic Message Listing

List messages in a folder using the messages listing API:

curl "https://your-emailengine.com/v1/account/example/messages?path=INBOX" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response:

{
"total": 300,
"page": 0,
"pages": 15,
"nextPageCursor": "imap_kcQIji3UobDDTxc",
"prevPageCursor": null,
"messages": [
{
"id": "AAAAAQAAAeE",
"uid": 12345,
"emailId": "1743d29c-b67d-4747-9016-b8850a5a39bd",
"threadId": "1743d29c-b67d-4747-9016-b8850a5a39bd",
"date": "2025-10-13T10:23:45.000Z",
"flags": ["\\Seen"],
"labels": ["\\Inbox"],
"unseen": false,
"flagged": false,
"draft": false,
"size": 45678,
"subject": "Meeting Tomorrow",
"from": {
"name": "John Doe",
"address": "john@example.com"
},
"to": [
{
"name": "Jane Smith",
"address": "jane@company.com"
}
],
"cc": [],
"bcc": [],
"messageId": "<abc123@example.com>",
"inReplyTo": null,
"attachments": [
{
"id": "AAAAAgAAAeEBAAAAAQAAAeE",
"contentType": "application/pdf",
"filename": "agenda.pdf"
}
]
}
]
}

Pagination

List messages with pagination:

async function listMessages(accountId, folderPath, page = 0, pageSize = 20) {
const params = new URLSearchParams({
path: folderPath,
page: page,
pageSize: pageSize
});

const response = await fetch(
`https://your-emailengine.com/v1/account/${accountId}/messages?${params}`,
{
headers: { 'Authorization': 'Bearer YOUR_ACCESS_TOKEN' }
}
);

return await response.json();
}

// List first page
const page1 = await listMessages('example', 'INBOX', 0, 20);
console.log(`Showing ${page1.messages.length} of ${page1.total} messages`);
console.log(`Page ${page1.page + 1} of ${page1.pages}`);

// List next page
const page2 = await listMessages('example', 'INBOX', 1, 20);

List All Messages

Iterate through all pages:

async function listAllMessages(accountId, folderPath) {
const allMessages = [];
let page = 0;
let hasMore = true;

while (hasMore) {
const response = await listMessages(accountId, folderPath, page, 100);

allMessages.push(...response.messages);

page++;
hasMore = page < response.pages;
}

return allMessages;
}

const allInbox = await listAllMessages('example', 'INBOX');
console.log(`Total messages: ${allInbox.length}`);

Filter by Flags

List only unseen messages:

curl "https://your-emailengine.com/v1/account/example/messages?path=INBOX&unseen=true" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

List flagged messages:

curl "https://your-emailengine.com/v1/account/example/messages?path=INBOX&flagged=true" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

JavaScript Example:

async function listUnreadMessages(accountId, folderPath) {
const params = new URLSearchParams({
path: folderPath,
unseen: 'true',
pageSize: 100
});

const response = await fetch(
`https://your-emailengine.com/v1/account/${accountId}/messages?${params}`,
{
headers: { 'Authorization': 'Bearer YOUR_ACCESS_TOKEN' }
}
);

return await response.json();
}

Fetching Messages

Get Message by ID

Fetch complete message details using the get message API:

curl "https://your-emailengine.com/v1/account/example/message/AAAAAQAAAeE" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response:

{
"id": "AAAAAQAAAeE",
"uid": 12345,
"emailId": "1743d29c-b67d-4747-9016-b8850a5a39bd",
"threadId": "1743d29c-b67d-4747-9016-b8850a5a39bd",
"date": "2025-10-13T10:23:45.000Z",
"flags": ["\\Seen"],
"labels": ["\\Inbox"],
"unseen": false,
"flagged": false,
"draft": false,
"size": 45678,
"subject": "Meeting Tomorrow",
"from": {
"name": "John Doe",
"address": "john@example.com"
},
"to": [
{
"name": "Jane Smith",
"address": "jane@company.com"
}
],
"messageId": "<abc123@example.com>",
"inReplyTo": null,
"headers": {
"date": ["Sun, 13 Oct 2025 10:23:45 +0000"],
"from": ["John Doe <john@example.com>"],
"to": ["Jane Smith <jane@company.com>"],
"subject": ["Meeting Tomorrow"],
"message-id": ["<abc123@example.com>"]
},
"text": {
"id": "AAAAAgAAAeETEXT",
"encodedSize": {
"plain": 52,
"html": 78
},
"plain": "Hi Jane,\n\nLet's meet tomorrow at 10am.\n\nBest,\nJohn",
"html": "<p>Hi Jane,</p><p>Let's meet tomorrow at 10am.</p><p>Best,<br>John</p>"
},
"attachments": [
{
"id": "AAAAAgAAAeEBAAAAAQAAAeE",
"contentType": "application/pdf",
"encodedSize": 45000,
"filename": "agenda.pdf"
}
]
}

JavaScript Example:

async function getMessage(accountId, messageId) {
const response = await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}`,
{
headers: { 'Authorization': 'Bearer YOUR_ACCESS_TOKEN' }
}
);

if (!response.ok) {
throw new Error(`Failed to fetch message: ${response.statusText}`);
}

return await response.json();
}

const message = await getMessage('example', 'AAAAAQAAAeE');
console.log(`From: ${message.from.address}`);
console.log(`Subject: ${message.subject}`);
console.log(`Body: ${message.text?.plain}`);

Get Message Source

Fetch raw RFC822 message source using the message source API:

curl "https://your-emailengine.com/v1/account/example/message/AAAAAQAAAeE/source" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Returns raw email:

From: John Doe <john@example.com>
To: Jane Smith <jane@company.com>
Subject: Meeting Tomorrow
Date: Sun, 13 Oct 2025 10:23:45 +0000
Message-ID: <abc123@example.com>
Content-Type: text/plain; charset=utf-8

Hi Jane,

Let's meet tomorrow at 10am.

Best,
John

JavaScript Example:

async function getMessageSource(accountId, messageId) {
const response = await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}/source`,
{
headers: { 'Authorization': 'Bearer YOUR_ACCESS_TOKEN' }
}
);

return await response.text();
}

const source = await getMessageSource('example', 'AAAAAQAAAeE');
console.log(source);

Moving Messages

Move to Different Folder

Move a message to another folder using the move message API:

curl -X PUT "https://your-emailengine.com/v1/account/example/message/AAAAAQAAAeE/move" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"path": "Work/Projects"
}'

Response:

{
"path": "Work/Projects",
"id": "BBBBBQAAAeE",
"uid": 5678
}

The response includes the destination path and, if provided by the server, the new message id and uid in the target folder.

JavaScript Example:

async function moveMessage(accountId, messageId, targetFolder) {
const response = await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}/move`,
{
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({ path: targetFolder })
}
);

return await response.json();
}

// Move to archive
await moveMessage('example', 'AAAAAQAAAeE', 'Archive/2025');

Archive Messages

Move messages to archive folder:

async function archiveMessage(accountId, messageId) {
// Find archive folder
const folders = await listMailboxes(accountId);
const archiveFolder = folders.find(f => f.specialUse === '\\Archive');

if (!archiveFolder) {
throw new Error('Archive folder not found');
}

return await moveMessage(accountId, messageId, archiveFolder.path);
}

Move to Trash

async function trashMessage(accountId, messageId) {
const folders = await listMailboxes(accountId);
const trashFolder = folders.find(f => f.specialUse === '\\Trash');

if (!trashFolder) {
throw new Error('Trash folder not found');
}

return await moveMessage(accountId, messageId, trashFolder.path);
}

Deleting Messages

Delete Message

Delete a message using the delete message API. By default, the message is moved to Trash. If the message is already in Trash (or if force=true is specified), it is permanently deleted.

curl -X DELETE "https://your-emailengine.com/v1/account/example/message/AAAAAQAAAeE" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response (moved to Trash):

{
"deleted": false,
"moved": {
"destination": "Trash",
"message": "AAAAAwAAAWg"
}
}

Response (permanently deleted):

{
"deleted": true
}

To force permanent deletion without moving to Trash first, add ?force=true to the URL (not supported for Gmail API accounts).

JavaScript Example:

async function deleteMessage(accountId, messageId) {
const response = await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}`,
{
method: 'DELETE',
headers: { 'Authorization': 'Bearer YOUR_ACCESS_TOKEN' }
}
);

return await response.json();
}

await deleteMessage('example', 'AAAAAQAAAeE');

Delete Multiple Messages

async function deleteMessages(accountId, messageIds) {
const results = [];

for (const messageId of messageIds) {
try {
const result = await deleteMessage(accountId, messageId);
results.push({ messageId, success: true });
} catch (err) {
results.push({ messageId, success: false, error: err.message });
}
}

return results;
}

const results = await deleteMessages('example', [
'AAAAAQAAAeE',
'AAAAAQAAAeF',
'AAAAAQAAAeG'
]);

Delete Behavior

The delete API has smart behavior:

  • Message not in Trash: Moves to Trash (recoverable)
  • Message already in Trash: Permanently deletes (not recoverable)
  • With ?force=true: Permanently deletes regardless of location (not supported for Gmail API)

This means you can safely use the delete endpoint in most cases - messages get a "second chance" in Trash before permanent deletion.

Updating Message Flags

Set Flags

Update message flags using the update message API:

curl -X PUT "https://your-emailengine.com/v1/account/example/message/AAAAAQAAAeE" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"flags": {
"add": ["\\Seen", "\\Flagged"],
"delete": []
}
}'

Response:

{
"flags": {
"add": ["\\Seen", "\\Flagged"]
}
}

The response echoes back the flag operations that were performed.

Common Flag Operations

Mark as read:

async function markAsRead(accountId, messageId) {
const response = await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}`,
{
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
flags: {
add: ['\\Seen']
}
})
}
);

return await response.json();
}

Mark as unread:

async function markAsUnread(accountId, messageId) {
return await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}`,
{
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
flags: {
delete: ['\\Seen']
}
})
}
).then(r => r.json());
}

Toggle flag/star:

async function toggleFlag(accountId, messageId, currentlyFlagged) {
return await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}`,
{
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
flags: currentlyFlagged
? { delete: ['\\Flagged'] }
: { add: ['\\Flagged'] }
})
}
).then(r => r.json());
}

Mark as answered:

async function markAsAnswered(accountId, messageId) {
return await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}`,
{
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
flags: {
add: ['\\Answered']
}
})
}
).then(r => r.json());
}

Batch Flag Updates

Update flags on multiple messages:

async function markAllAsRead(accountId, messageIds) {
const results = [];

for (const messageId of messageIds) {
try {
await markAsRead(accountId, messageId);
results.push({ messageId, success: true });
} catch (err) {
results.push({ messageId, success: false, error: err.message });
}
}

return results;
}

// Mark all unread messages as read
const unread = await listUnreadMessages('example', 'INBOX');
const messageIds = unread.messages.map(m => m.id);
await markAllAsRead('example', messageIds);

Working with Gmail Labels and Outlook Categories

EmailEngine uses the labels array for both Gmail labels and Microsoft Outlook/Graph API categories.

Gmail Labels:

  • Labels must be pre-created in Gmail
  • Labels map to folders (e.g., label "Work" corresponds to a folder)
  • Used with Gmail IMAP and Gmail API accounts

Microsoft Outlook Categories:

  • Categories are automatically created when you set them (no pre-creation needed)
  • Categories are not folders - they're an additional filter/tag
  • Categories don't map to mailbox folders (unlike Gmail labels)
  • Only available when using Microsoft Graph API backend

Add Labels/Categories

For Gmail or Microsoft Graph accounts, add labels or categories to a message:

curl -X PUT "https://your-emailengine.com/v1/account/example/message/AAAAAQAAAeE" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"labels": {
"add": ["Work", "Important"]
}
}'

Example - Add categories to Outlook message:

curl -X PUT "https://your-emailengine.com/v1/account/outlook-account/message/AAMkADU" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"labels": {
"add": ["Blue category", "Red category"]
}
}'
Microsoft Graph Auto-Creation

When using Microsoft Graph API, categories are automatically created if they don't exist. You don't need to pre-create them in Outlook.

Remove Labels/Categories

async function removeLabel(accountId, messageId, label) {
return await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}`,
{
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
labels: {
delete: [label]
}
})
}
).then(r => r.json());
}

Set Labels/Categories (Replace All)

async function setLabels(accountId, messageId, labels) {
// First, get current labels
const message = await getMessage(accountId, messageId);
const currentLabels = message.labels || [];

// Determine which to add and remove
const toAdd = labels.filter(l => !currentLabels.includes(l));
const toRemove = currentLabels.filter(l => !labels.includes(l));

if (toAdd.length === 0 && toRemove.length === 0) {
return message; // No changes needed
}

return await fetch(
`https://your-emailengine.com/v1/account/${accountId}/message/${messageId}`,
{
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
labels: {
add: toAdd,
delete: toRemove
}
})
}
).then(r => r.json());
}

// Set exact labels (replaces all existing)
await setLabels('example', 'AAAAAQAAAeE', ['Work', 'Urgent']);

Key Differences: Gmail Labels vs Outlook Categories

FeatureGmail LabelsOutlook Categories
Pre-creation requiredYes - must exist in GmailNo - auto-created when set
Folder mappingYes - labels map to foldersNo - separate filter/tag system
Backend supportGmail IMAP, Gmail APIMicrosoft Graph API only
Use caseOrganize messages into foldersAdd color-coded tags to messages
Color supportNo colorsColors assigned by Outlook (not via API)
Delete/renameYes - via GmailNo - use Outlook directly
Important

Outlook categories are not a replacement for folders. They're an additional organizational layer. Unlike Gmail where a label creates a folder, Outlook categories are independent tags that don't affect folder structure.

Outlook Category Limitations

EmailEngine can only work with category names, not colors. When you assign a new category name to a message, Outlook automatically creates the category with a default color. To change colors, rename, or delete categories, you must use Outlook directly - these operations are not available through EmailEngine.

Common Patterns

Process Unread Messages

async function processUnreadMessages(accountId, processor) {
const unread = await listUnreadMessages(accountId, 'INBOX');

for (const message of unread.messages) {
try {
// Get full message content
const fullMessage = await getMessage(accountId, message.id);

// Process the message
await processor(fullMessage);

// Mark as read
await markAsRead(accountId, message.id);

console.log(`Processed message: ${message.subject}`);
} catch (err) {
console.error(`Failed to process ${message.id}:`, err);
}
}
}

// Usage
await processUnreadMessages('example', async (message) => {
// Your processing logic
console.log(`Processing: ${message.subject}`);
// Extract data, send to API, etc.
});

Auto-Archive Old Messages

async function autoArchiveOldMessages(accountId, daysOld = 90) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);

const messages = await listAllMessages(accountId, 'INBOX');

let archived = 0;

for (const message of messages) {
const messageDate = new Date(message.date);

if (messageDate < cutoffDate) {
try {
await archiveMessage(accountId, message.id);
archived++;
} catch (err) {
console.error(`Failed to archive ${message.id}:`, err);
}
}
}

console.log(`Archived ${archived} messages older than ${daysOld} days`);
return archived;
}

// Archive messages older than 90 days
await autoArchiveOldMessages('example', 90);

Sync Flags to Database

async function syncMessageFlags(accountId, folderPath, db) {
const messages = await listAllMessages(accountId, folderPath);

for (const message of messages) {
await db.updateMessage({
accountId: accountId,
messageId: message.id,
flags: message.flags,
unseen: message.unseen,
flagged: message.flagged,
answered: message.answered
});
}

console.log(`Synced ${messages.length} message flags`);
}