Issue Description
The JMAP query (using jmap-client-ts):
const query = await client.email_query({
accountId: "f",
filter: {
hasAttachment: true
},
limit: 2000,
calculateTotal: true
});
Returns incomplete results (only 32 emails instead of > 100) in my scenario. I am not quite sure whether this is really a bug or a broken database on my site.
Expected Behavior
Stalwart should return all emails containing attachments.
Actual Behavior
Stalwart returns exactly 32 emails. The weird thing is that most of them are relatively recent, then there is a huge gap, and the last few are from August 2025.
Reproduction Steps
- Setup Stalwart
- Have some emails in an inbox
- Use the provided TypeScript file
Stalwart Version
v0.16.x
Installation Method
Docker
Database Backend
PostgreSQL
Blob Storage
PostgreSQL
Search Engine
PostgreSQL
Directory Backend
Internal
Additional Context
The same happens when using GitHub - root-fr/jmap-webmail: Modern JMAP webmail client built for Stalwart Mail Server. Privacy-focused, responsive, with real-time updates. · GitHub as a webmail client. Filtering for emails containing attachments only returns 32 emails (the same).
Script:
import { Client } from 'jmap-client-ts';
interface Transport {
get<ResponseType>(url: string, headers: { [headerName: string]: string }): Promise<ResponseType>;
post<ResponseType>(url: string, content: unknown, headers: { [headerName: string]: string }): Promise<ResponseType>;
}
const transport: Transport = {
get: async <ResponseType>(url: string, headers: { [headerName: string]: string }) => {
const response = await fetch(url, { method: 'GET', headers });
if (!response.ok) {
throw new Error(`GET ${url} failed with status ${response.status}`);
}
return (await response.json()) as ResponseType;
},
post: async <ResponseType>(url: string, content: unknown, headers: { [headerName: string]: string }) => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(content)
});
if (!response.ok) {
throw new Error(`POST ${url} failed with status ${response.status}`);
}
return (await response.json()) as ResponseType;
}
};
async function markEmailsWithAttachmentsAsUnread() {
const API_TOKEN = process.env.JMAP_TOKEN || 'YOUR_ACCESS_TOKEN_HERE';
const SESSION_URL = 'https://HOST/.well-known/jmap';
const client = new Client({
accessToken: API_TOKEN,
sessionUrl: SESSION_URL,
transport
});
await client.fetchSession();
const emailIds: string[] = [];
let position = 0;
while (true) {
const query = await client.email_query({
accountId: "f",
filter: {
hasAttachment: true
},
position: position,
limit: 2000,
calculateTotal: true
});
emailIds.push(...query.ids);
position += query.ids.length;
if (query.ids.length === 0 || (query.total !== undefined && emailIds.length >= query.total)) {
break;
}
}
if (emailIds.length === 0) {
console.log('No emails with attachments found.');
return;
}
const update = Object.fromEntries(
emailIds.map((id) => [id, { 'keywords/$seen': null }])
);
await client.email_set({
accountId: "f",
update
});
console.log(`Successfully marked ${emailIds.length} email(s) as unread.`);
}
markEmailsWithAttachmentsAsUnread().catch((error) => {
console.error('Error while marking emails as unread:', error);
process.exitCode = 1;
});
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
on