@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
159 lines • 6.14 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { decodeBase64 } from "../../utils/base64.js";
import { gmailBase, googleGet } from "../google/client.js";
import { compactJsonBytes } from "../render/json.js";
/**
* One page of message stubs for a label or query, and the next page's token,
* null on the last page. Gmail may return fewer than `maxResults` stubs and
* still name a next page. Mirrors Python's `list_message_page`.
*/
export async function listMessagePage(tokenManager, opts = {}) {
const params = { maxResults: opts.maxResults ?? 50 };
if (opts.labelId !== undefined && opts.labelId !== null && opts.labelId !== '') {
params.labelIds = opts.labelId;
}
if (opts.query !== undefined && opts.query !== null && opts.query !== '') {
params.q = opts.query;
}
const url = `${gmailBase(tokenManager)}/users/me/messages`;
const data = (await googleGet(tokenManager, url, params));
return [data.messages ?? [], data.nextPageToken ?? null];
}
export async function listMessages(tokenManager, opts = {}) {
const [stubs] = await listMessagePage(tokenManager, opts);
return stubs;
}
export async function getMessageRaw(tokenManager, messageId) {
const url = `${gmailBase(tokenManager)}/users/me/messages/${messageId}?format=full`;
return (await googleGet(tokenManager, url));
}
function base64UrlDecodeToBytes(input) {
const padded = input + '=='.slice((input.length + 2) % 4);
return decodeBase64(padded.replace(/-/g, '+').replace(/_/g, '/'));
}
const TEXT_DECODER = new TextDecoder('utf-8', { fatal: false });
export function decodeBody(payload) {
if (payload === undefined)
return '';
if (payload.mimeType === 'text/plain') {
const data = payload.body?.data ?? '';
if (data !== '') {
return TEXT_DECODER.decode(base64UrlDecodeToBytes(data));
}
}
for (const part of payload.parts ?? []) {
const text = decodeBody(part);
if (text !== '')
return text;
}
return '';
}
export function extractHeader(headers, name) {
if (headers === undefined)
return '';
const lower = name.toLowerCase();
for (const h of headers) {
if ((h.name ?? '').toLowerCase() === lower)
return h.value ?? '';
}
return '';
}
export function parseAddress(raw) {
if (raw.includes('<') && raw.includes('>')) {
const lt = raw.indexOf('<');
const gt = raw.indexOf('>');
const name = raw.slice(0, lt).trim().replace(/^"|"$/g, '');
const email = raw.slice(lt + 1, gt).trim();
return { name, email };
}
return { name: '', email: raw.trim() };
}
function parseAddressList(raw) {
if (raw === '')
return [];
return raw.split(',').map((a) => parseAddress(a.trim()));
}
export async function getAttachment(tokenManager, messageId, attachmentId) {
const url = `${gmailBase(tokenManager)}/users/me/messages/${messageId}/attachments/${attachmentId}`;
const data = (await googleGet(tokenManager, url));
const raw = data.data ?? '';
return base64UrlDecodeToBytes(raw);
}
export function extractAttachments(payload) {
if (payload === undefined)
return [];
const attachments = [];
const parts = payload.parts ?? [];
for (const part of parts) {
const filename = part.filename ?? '';
const body = part.body ?? {};
const attachmentId = body.attachmentId ?? '';
if (filename !== '' && attachmentId !== '') {
attachments.push({
filename,
attachmentId,
size: body.size ?? 0,
mimeType: part.mimeType ?? '',
});
}
for (const sub of part.parts ?? []) {
const fn = sub.filename ?? '';
const bd = sub.body ?? {};
const aid = bd.attachmentId ?? '';
if (fn !== '' && aid !== '') {
attachments.push({
filename: fn,
attachmentId: aid,
size: bd.size ?? 0,
mimeType: sub.mimeType ?? '',
});
}
}
}
return attachments;
}
export async function getMessageProcessed(tokenManager, messageId) {
const raw = await getMessageRaw(tokenManager, messageId);
return processMessage(raw);
}
export function processMessage(raw) {
const headers = raw.payload?.headers ?? [];
const bodyText = decodeBody(raw.payload);
return {
id: raw.id ?? '',
thread_id: raw.threadId ?? '',
from: parseAddress(extractHeader(headers, 'From')),
to: parseAddressList(extractHeader(headers, 'To')),
cc: parseAddressList(extractHeader(headers, 'Cc')),
subject: extractHeader(headers, 'Subject'),
date: extractHeader(headers, 'Date'),
body_text: bodyText,
snippet: raw.snippet ?? '',
labels: raw.labelIds ?? [],
attachments: extractAttachments(raw.payload).map((a) => ({
id: a.attachmentId,
filename: a.filename,
path: `attachments/${a.attachmentId}_${a.filename}`,
mime_type: a.mimeType,
size: a.size,
})),
};
}
// The single renderer behind both read and the readdir-time size, so
// stat().size == len(read()) by construction.
export function messageJsonBytes(raw) {
return compactJsonBytes(processMessage(raw));
}
//# sourceMappingURL=messages.js.map