diff --git a/lib/services/msgraph-client.ts b/lib/services/msgraph-client.ts index fc962c1..258de81 100644 --- a/lib/services/msgraph-client.ts +++ b/lib/services/msgraph-client.ts @@ -408,6 +408,35 @@ export class MsGraphClient { throw new Error(`Graph move failed ${res.status}: ${text}`); } + /** + * Fetch the raw photo bytes for a Microsoft Graph user. + * Returns { bytes, contentType } on 200, null when the user has no photo (404), + * and throws on any other non-2xx response so the caller can map to 502/503. + * + * Does NOT reuse fetchJson (which is JSON-only); issues its own fetch for binary data. + * No retry logic — photo fetches are best-effort per D-26. + */ + async getUserPhotoBytes(userId: string): Promise<{ bytes: ArrayBuffer; contentType: string } | null> { + const token = await this.getToken(); + const url = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(userId)}/photo/$value`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (res.status === 404) { + return null; + } + + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`Graph photo error ${res.status} for user ${userId}: ${text}`); + } + + const bytes = await res.arrayBuffer(); + const contentType = res.headers.get('content-type') ?? 'image/jpeg'; + return { bytes, contentType }; + } + /** * Get calendar events for a user in a date range (paginated). * Returns empty array and logs if the mailbox is not Exchange Online (graceful degradation).