- {folder === 'inbox' && (
+ {activeAccount?.providerId === 'google' && folder === 'inbox' && (
0} />
)}
diff --git a/apps/mail/components/ui/nav-user.tsx b/apps/mail/components/ui/nav-user.tsx
index 426175edc..ab75157da 100644
--- a/apps/mail/components/ui/nav-user.tsx
+++ b/apps/mail/components/ui/nav-user.tsx
@@ -51,8 +51,7 @@ import { toast } from 'sonner';
import Link from 'next/link';
export function NavUser() {
- const { data: session, refetch } = useSession();
- const router = useRouter();
+ const { data: session, refetch: refetchSession } = useSession();
const { data, refetch: refetchConnections } = useConnections();
const [isRendered, setIsRendered] = useState(false);
const [showPricing, setShowPricing] = useState(false);
@@ -60,9 +59,6 @@ export function NavUser() {
const t = useTranslations();
const { state } = useSidebar();
const trpc = useTRPC();
- const { refetch: refetchStats } = useStats();
- const [{ refetch: refetchThreads }] = useThreads();
- const { refetch: refetchLabels } = useLabels();
const { mutateAsync: setDefaultConnection } = useMutation(
trpc.connections.setDefault.mutationOptions(),
);
@@ -98,20 +94,13 @@ export function NavUser() {
useEffect(() => setIsRendered(true), []);
- const refetchBrainLabels = useCallback(() => {
- queryClient.invalidateQueries({ queryKey: trpc.brain.getLabels.queryKey() });
- }, [queryClient]);
-
const handleAccountSwitch = (connectionId: string) => async () => {
if (connectionId === session?.connectionId) return;
await setDefaultConnection({ connectionId });
- refetch();
- refetchConnections();
- refetchThreads();
- refetchLabels();
- refetchStats();
- refetchBrainState();
- refetchBrainLabels();
+ await refetchConnections();
+ refetchSession();
+ // TODO: fix this cache issue, for now this is a quick fix to hard refresh the page
+ window.location.href = pathname;
};
const handleLogout = async () => {
diff --git a/apps/mail/lib/constants.ts b/apps/mail/lib/constants.tsx
similarity index 72%
rename from apps/mail/lib/constants.ts
rename to apps/mail/lib/constants.tsx
index d261f73a2..d7f6f8d5d 100644
--- a/apps/mail/lib/constants.ts
+++ b/apps/mail/lib/constants.tsx
@@ -1,3 +1,4 @@
+import { GmailColor, OutlookColor } from '../components/icons/icons';
import { env } from '@/lib/env';
export const I18N_LOCALE_COOKIE_NAME = 'i18n:locale';
@@ -14,15 +15,15 @@ export const CACHE_BURST_KEY = 'cache-burst:v0.0.2';
export const emailProviders = [
{
- name: 'Google',
- icon: 'M11.99 13.9v-3.72h9.36c.14.63.25 1.22.25 2.05c0 5.71-3.83 9.77-9.6 9.77c-5.52 0-10-4.48-10-10S6.48 2 12 2c2.7 0 4.96.99 6.69 2.61l-2.84 2.76c-.72-.68-1.98-1.48-3.85-1.48c-3.31 0-6.01 2.75-6.01 6.12s2.7 6.12 6.01 6.12c3.83 0 5.24-2.65 5.5-4.22h-5.51z',
+ name: 'Gmail',
+ icon: GmailColor,
providerId: 'google',
},
- // {
- // name: 'Microsoft',
- // icon: 'M11.99 13.9v-3.72h9.36c.14.63.25 1.22.25 2.05c0 5.71-3.83 9.77-9.6 9.77c-5.52 0-10-4.48-10-10S6.48 2 12 2c2.7 0 4.96.99 6.69 2.61l-2.84 2.76c-.72-.68-1.98-1.48-3.85-1.48c-3.31 0-6.01 2.75-6.01 6.12s2.7 6.12 6.01 6.12c3.83 0 5.24-2.65 5.5-4.22h-5.51z',
- // providerId: 'microsoft',
- // },
+ {
+ name: 'Outlook',
+ icon: OutlookColor,
+ providerId: 'microsoft',
+ },
] as const;
interface GmailColor {
diff --git a/apps/mail/package.json b/apps/mail/package.json
index 9fb90da9c..e4edf237e 100644
--- a/apps/mail/package.json
+++ b/apps/mail/package.json
@@ -86,7 +86,7 @@
"@zero/server": "workspace:*",
"ai": "^4.3.9",
"autumn-js": "0.0.22",
- "better-auth": "1.2.7",
+ "better-auth": "1.2.8-beta.7",
"canvas-confetti": "1.9.3",
"cheerio": "1.0.0",
"class-variance-authority": "0.7.1",
diff --git a/apps/server/package.json b/apps/server/package.json
index 79795baaf..5f9492914 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -16,6 +16,8 @@
"dependencies": {
"@ai-sdk/openai": "^1.3.21",
"@hono/trpc-server": "^0.3.4",
+ "@microsoft/microsoft-graph-client": "^3.0.7",
+ "@microsoft/microsoft-graph-types": "^2.40.0",
"@react-email/components": "^0.0.38",
"@react-email/render": "^1.1.0",
"@trpc/client": "^11.1.2",
@@ -32,6 +34,7 @@
"googleapis": "^148.0.0",
"he": "^1.2.0",
"hono": "^4.7.8",
+ "hono-party": "^0.0.12",
"jsonrepair": "^3.12.0",
"mimetext": "^3.0.27",
"partyserver": "^0.0.71",
diff --git a/apps/server/src/lib/auth-providers.ts b/apps/server/src/lib/auth-providers.ts
index cdfee61d8..da6cab93a 100644
--- a/apps/server/src/lib/auth-providers.ts
+++ b/apps/server/src/lib/auth-providers.ts
@@ -50,25 +50,31 @@ export const authProviders = (env: Record
): ProviderConfig[] =>
},
required: true,
},
- // {
- // id: 'microsoft',
- // name: 'Microsoft',
- // requiredEnvVars: ['MICROSOFT_CLIENT_ID', 'MICROSOFT_CLIENT_SECRET'],
- // envVarInfo: [
- // { name: 'MICROSOFT_CLIENT_ID', source: 'Microsoft Azure App ID' },
- // { name: 'MICROSOFT_CLIENT_SECRET', source: 'Microsoft Azure App Password' },
- // ],
- // config: {
- // clientId: env.MICROSOFT_CLIENT_ID!,
- // clientSecret: env.MICROSOFT_CLIENT_SECRET!,
- // redirectUri: env.MICROSOFT_REDIRECT_URI!,
- // scope: ['https://graph.microsoft.com/User.Read', 'offline_access'],
- // authority: 'https://login.microsoftonline.com/common',
- // responseType: 'code',
- // prompt: 'consent',
- // loginHint: 'email',
- // },
- // },
+ {
+ id: 'microsoft',
+ name: 'Microsoft',
+ requiredEnvVars: ['MICROSOFT_CLIENT_ID', 'MICROSOFT_CLIENT_SECRET'],
+ envVarInfo: [
+ { name: 'MICROSOFT_CLIENT_ID', source: 'Microsoft Azure App ID' },
+ { name: 'MICROSOFT_CLIENT_SECRET', source: 'Microsoft Azure App Password' },
+ ],
+ config: {
+ clientId: env.MICROSOFT_CLIENT_ID,
+ clientSecret: env.MICROSOFT_CLIENT_SECRET,
+ redirectUri: env.MICROSOFT_REDIRECT_URI,
+ scope: [
+ 'https://graph.microsoft.com/User.Read',
+ 'https://graph.microsoft.com/Mail.ReadWrite',
+ 'https://graph.microsoft.com/Mail.Send',
+ 'offline_access',
+ ],
+ authority: 'https://login.microsoftonline.com/common',
+ responseType: 'code',
+ prompt: 'consent',
+ loginHint: 'email',
+ disableProfilePhoto: true,
+ },
+ },
];
export function isProviderEnabled(provider: ProviderConfig, env: Record): boolean {
diff --git a/apps/server/src/lib/auth.ts b/apps/server/src/lib/auth.ts
index 330ee96d6..5ce70f717 100644
--- a/apps/server/src/lib/auth.ts
+++ b/apps/server/src/lib/auth.ts
@@ -31,7 +31,12 @@ const connectionHandlerHook = async (account: Account) => {
}
const driver = createDriver(account.providerId, {
- auth: { accessToken: account.accessToken, refreshToken: account.refreshToken, email: '' },
+ auth: {
+ accessToken: account.accessToken,
+ refreshToken: account.refreshToken,
+ userId: account.userId,
+ email: '',
+ },
});
const userInfo = await driver.getUserInfo().catch(() => {
diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts
index bed4e1738..9b17a13b4 100644
--- a/apps/server/src/lib/driver/google.ts
+++ b/apps/server/src/lib/driver/google.ts
@@ -34,7 +34,6 @@ export class GoogleMailManager implements MailManager {
this.gmail = google.gmail({ version: 'v1', auth: this.auth });
}
-
public getScope(): string {
return [
'https://www.googleapis.com/auth/gmail.modify',
@@ -42,7 +41,6 @@ export class GoogleMailManager implements MailManager {
'https://www.googleapis.com/auth/userinfo.email',
].join(' ');
}
-
public getAttachment(messageId: string, attachmentId: string) {
return this.withErrorHandler(
'getAttachment',
@@ -62,7 +60,6 @@ export class GoogleMailManager implements MailManager {
{ messageId, attachmentId },
);
}
-
public getEmailAliases() {
return this.withErrorHandler('getEmailAliases', async () => {
const profile = await this.gmail.users.getProfile({
@@ -95,7 +92,6 @@ export class GoogleMailManager implements MailManager {
return aliases;
});
}
-
public markAsRead(threadIds: string[]) {
return this.withErrorHandler(
'markAsRead',
@@ -105,7 +101,6 @@ export class GoogleMailManager implements MailManager {
{ threadIds },
);
}
-
public markAsUnread(threadIds: string[]) {
return this.withErrorHandler(
'markAsUnread',
@@ -115,7 +110,6 @@ export class GoogleMailManager implements MailManager {
{ threadIds },
);
}
-
public getUserInfo() {
return this.withErrorHandler(
'getUserInfo',
@@ -132,7 +126,6 @@ export class GoogleMailManager implements MailManager {
{},
);
}
-
public getTokens(code: string) {
return this.withErrorHandler(
'getTokens',
@@ -143,7 +136,6 @@ export class GoogleMailManager implements MailManager {
{ code },
);
}
-
public count() {
return this.withErrorHandler(
'count',
@@ -174,7 +166,6 @@ export class GoogleMailManager implements MailManager {
{ email: this.config.auth?.email },
);
}
-
public list(params: {
folder: string;
query?: string;
@@ -209,7 +200,6 @@ export class GoogleMailManager implements MailManager {
{ folder, q, maxResults, _labelIds, pageToken, email: this.config.auth?.email },
);
}
-
public get(id: string) {
return this.withErrorHandler(
'get',
@@ -353,7 +343,6 @@ export class GoogleMailManager implements MailManager {
{ id, email: this.config.auth?.email },
);
}
-
public create(data: IOutgoingMessage) {
return this.withErrorHandler(
'create',
@@ -371,7 +360,6 @@ export class GoogleMailManager implements MailManager {
{ data, email: this.config.auth?.email },
);
}
-
public delete(id: string) {
return this.withErrorHandler(
'delete',
@@ -382,7 +370,6 @@ export class GoogleMailManager implements MailManager {
{ id },
);
}
-
public normalizeIds(ids: string[]) {
return this.withSyncErrorHandler(
'normalizeIds',
@@ -395,7 +382,6 @@ export class GoogleMailManager implements MailManager {
{ ids },
);
}
-
public modifyLabels(
threadIds: string[],
options: { addLabels: string[]; removeLabels: string[] },
@@ -411,7 +397,6 @@ export class GoogleMailManager implements MailManager {
{ threadIds, options },
);
}
-
public sendDraft(draftId: string, data: IOutgoingMessage) {
return this.withErrorHandler(
'sendDraft',
@@ -663,9 +648,6 @@ export class GoogleMailManager implements MailManager {
return false;
}
}
-
- // ===============================================
-
private async modifyThreadLabels(
threadIds: string[],
requestBody: gmail_v1.Schema$ModifyThreadRequest,
@@ -990,7 +972,6 @@ export class GoogleMailManager implements MailManager {
rawMessage: draft.message,
};
}
-
private async withErrorHandler(
operation: string,
fn: () => Promise | T,
@@ -1037,16 +1018,15 @@ export class GoogleMailManager implements MailManager {
}
}
- private findAttachments(parts: any[]): any[] {
- let results: any[] = [];
+ private findAttachments(parts: gmail_v1.Schema$MessagePart[]): gmail_v1.Schema$MessagePart[] {
+ let results: gmail_v1.Schema$MessagePart[] = [];
for (const part of parts) {
if (part.filename && part.filename.length > 0) {
const contentDisposition =
- part.headers?.find((h: any) => h.name?.toLowerCase() === 'content-disposition')?.value ||
- '';
+ part.headers?.find((h) => h.name?.toLowerCase() === 'content-disposition')?.value || '';
const isInline = contentDisposition.toLowerCase().includes('inline');
- const hasContentId = part.headers?.some((h: any) => h.name?.toLowerCase() === 'content-id');
+ const hasContentId = part.headers?.some((h) => h.name?.toLowerCase() === 'content-id');
if (!isInline || (isInline && !hasContentId)) {
results.push(part);
diff --git a/apps/server/src/lib/driver/index.ts b/apps/server/src/lib/driver/index.ts
index 0d578c073..36fb1a079 100644
--- a/apps/server/src/lib/driver/index.ts
+++ b/apps/server/src/lib/driver/index.ts
@@ -1,9 +1,10 @@
import type { MailManager, ManagerConfig } from './types';
+import { OutlookMailManager } from './microsoft';
import { GoogleMailManager } from './google';
const supportedProviders = {
google: GoogleMailManager,
- // microsoft: microsoftDriver,
+ microsoft: OutlookMailManager,
};
export const createDriver = (
diff --git a/apps/server/src/lib/driver/microsoft.ts b/apps/server/src/lib/driver/microsoft.ts
index 572691408..5330e972b 100644
--- a/apps/server/src/lib/driver/microsoft.ts
+++ b/apps/server/src/lib/driver/microsoft.ts
@@ -1,446 +1,1194 @@
-/**
- * NEEDS TO BE FIXED, COMMENTED OUT FOR NOW
- */
+import {
+ deleteActiveConnection,
+ FatalErrors,
+ fromBase64Url,
+ sanitizeContext,
+ StandardizedError,
+} from './utils';
+import type {
+ OutlookCategory as Category,
+ MailFolder,
+ Message,
+ User,
+} from '@microsoft/microsoft-graph-types';
+import type { IOutgoingMessage, Label, ParsedMessage } from '../../types';
+import { sanitizeTipTapHtml } from '../sanitize-tip-tap-html';
+import { Client } from '@microsoft/microsoft-graph-client';
+import type { MailManager, ManagerConfig } from './types';
+import { getContext } from 'hono/context-storage';
+import type { CreateDraftData } from '../schemas';
+import type { HonoContext } from '../../ctx';
+import * as he from 'he';
-// import { IOutgoingMessage, Sender, type ParsedMessage, type InitialThread } from '@/types';
-// import { parseAddressList, parseFrom, wasSentWithTLS } from '@/lib/email-utils';
-// import { fromBinary, fromBase64Url, findHtmlBody } from '@/lib/driver-utils';
-// import { Conversation } from '@microsoft/microsoft-graph-types';
-// import type { Message } from '@microsoft/microsoft-graph-types';
-// import { Client } from '@microsoft/microsoft-graph-client';
-// import { filterSuggestions } from '@/lib/filter';
-// import { cleanSearchValue } from '@/lib/utils';
-// import { IConfig, MailManager } from './types';
-// import { createMimeMessage } from 'mimetext';
-// import * as he from 'he';
+export class OutlookMailManager implements MailManager {
+ private graphClient: Client;
-// export const driver = async (config: IConfig): Promise => {
-// const getClient = (accessToken: string) => {
-// return Client.initWithMiddleware({
-// authProvider: {
-// getAccessToken: async () => accessToken,
-// },
-// });
-// };
+ constructor(public config: ManagerConfig) {
+ const getAccessToken = async () => {
+ const c = getContext();
+ const data = await c.var.auth.api.getAccessToken({
+ body: {
+ providerId: 'microsoft',
+ userId: config.auth.userId,
+ // accountId: config.auth.accountId,
+ },
+ headers: c.req.raw.headers,
+ });
+ if (!data.accessToken) throw new Error('Failed to get access token');
+ return data.accessToken;
+ };
-// const getScope = () =>
-// 'https://graph.microsoft.com/User.Read https://graph.microsoft.com/Mail.ReadWrite https://graph.microsoft.com/Mail.Send offline_access';
+ this.graphClient = Client.initWithMiddleware({
+ authProvider: {
+ getAccessToken,
+ },
+ });
+ }
-// const parseMessage = (message: Message): ParsedMessage => {
-// const headers = message.internetMessageHeaders || [];
-// const dateHeader = headers.find((h) => h.name?.toLowerCase() === 'date');
-// const receivedOn = (dateHeader?.value ||
-// message.receivedDateTime ||
-// new Date().toISOString()) as string;
-// const sender = headers.find((h) => h.name?.toLowerCase() === 'from')?.value || 'Failed';
-// const subject = headers.find((h) => h.name?.toLowerCase() === 'subject')?.value || '';
-// const references = headers.find((h) => h.name?.toLowerCase() === 'references')?.value || '';
-// const inReplyTo = headers.find((h) => h.name?.toLowerCase() === 'in-reply-to')?.value || '';
-// const messageId = headers.find((h) => h.name?.toLowerCase() === 'message-id')?.value || '';
-// const listUnsubscribe = headers.find(
-// (h) => h.name?.toLowerCase() === 'list-unsubscribe',
-// )?.value;
-// const listUnsubscribePost = headers.find(
-// (h) => h.name?.toLowerCase() === 'list-unsubscribe-post',
-// )?.value;
-// const replyTo = headers.find((h) => h.name?.toLowerCase() === 'reply-to')?.value;
-// const to = headers.find((h) => h.name?.toLowerCase() === 'to')?.value || '';
-// const cc = headers.find((h) => h.name?.toLowerCase() === 'cc')?.value || '';
-// const receivedHeaders = headers
-// .filter((h) => h.name?.toLowerCase() === 'received')
-// .map((h) => h.value || '');
-// const hasTLSReport = headers.some((h) => h.name?.toLowerCase() === 'tls-report');
+ public getScope(): string {
+ return [
+ 'https://graph.microsoft.com/User.Read',
+ 'https://graph.microsoft.com/Mail.ReadWrite',
+ 'https://graph.microsoft.com/Mail.Send',
+ 'offline_access',
+ ].join(' ');
+ }
+ public getAttachment(messageId: string, attachmentId: string) {
+ return this.withErrorHandler(
+ 'getAttachment',
+ async () => {
+ const response = await this.graphClient
+ .api(`/me/messages/${messageId}/attachments/${attachmentId}`)
+ .get();
-// return {
-// id: message.id || 'ERROR',
-// bcc: [],
-// threadId: message.conversationId || '',
-// title: message.subject || 'ERROR',
-// tls: wasSentWithTLS(receivedHeaders) || !!hasTLSReport,
-// tags: message.categories?.map((c) => ({ id: c, name: c })) || [],
-// // listUnsubscribe,
-// // listUnsubscribePost,
-// // replyTo,
-// references,
-// inReplyTo,
-// sender: {
-// email: sender,
-// name: sender,
-// },
-// unread: !message.isRead,
-// to: parseAddressList(to),
-// cc: cc ? parseAddressList(cc) : null,
-// receivedOn,
-// subject: subject ? subject.replace(/"/g, '').trim() : '(no subject)',
-// messageId,
-// body: message.body?.content || '',
-// processedHtml: message.body?.content || '',
-// blobUrl: '',
-// };
-// };
+ const attachment = response;
-// const parseOutgoing = async ({
-// to,
-// subject,
-// message,
-// attachments,
-// headers,
-// cc,
-// bcc,
-// }: IOutgoingMessage) => {
-// const msg = createMimeMessage();
-// const fromEmail = config.auth?.email || 'nobody@example.com';
-// msg.setSender({ name: '', addr: fromEmail });
+ if (!attachment || !attachment.contentBytes) {
+ throw new Error('Attachment data not found');
+ }
-// const uniqueRecipients = new Set();
+ const base64 = fromBase64Url(attachment.contentBytes);
-// if (!Array.isArray(to) || to.length === 0) {
-// throw new Error('Recipient address required');
-// }
+ return base64;
+ },
+ { messageId, attachmentId },
+ );
+ }
+ public getEmailAliases() {
+ return this.withErrorHandler('getEmailAliases', async () => {
+ const user: User = await this.graphClient.api('/me').select('mail,userPrincipalName').get();
+ const primaryEmail = user.mail || user.userPrincipalName || '';
-// const toRecipients = to
-// .filter((recipient) => {
-// if (!recipient || !recipient.email) return false;
-// const email = recipient.email.toLowerCase();
-// if (!uniqueRecipients.has(email)) {
-// uniqueRecipients.add(email);
-// return true;
-// }
-// return false;
-// })
-// .map((recipient) => ({
-// name: recipient.name || '',
-// addr: recipient.email,
-// }));
+ const aliases: { email: string; name?: string; primary?: boolean }[] = [
+ { email: primaryEmail, primary: true },
+ ];
-// if (toRecipients.length === 0) {
-// throw new Error('No valid recipients found in To field');
-// }
+ return aliases;
+ });
+ }
+ public markAsRead(messageIds: string[]) {
+ return this.withErrorHandler(
+ 'markAsRead',
+ async () => {
+ await this.modifyMessageReadStatus(messageIds, true);
+ },
+ { messageIds },
+ );
+ }
+ public markAsUnread(messageIds: string[]) {
+ return this.withErrorHandler(
+ 'markAsUnread',
+ async () => {
+ await this.modifyMessageReadStatus(messageIds, false);
+ },
+ { messageIds },
+ );
+ }
+ private async modifyMessageReadStatus(messageIds: string[], isRead: boolean) {
+ if (messageIds.length === 0) {
+ return;
+ }
-// msg.setTo(toRecipients);
+ const batchRequests = messageIds.map((id, index) => ({
+ id: `${index}`,
+ method: 'PATCH',
+ url: `/me/messages/${id}`,
+ body: { isRead: isRead },
+ headers: { 'Content-Type': 'application/json' },
+ }));
-// if (Array.isArray(cc) && cc.length > 0) {
-// const ccRecipients = cc
-// .filter((recipient) => {
-// const email = recipient.email.toLowerCase();
-// if (!uniqueRecipients.has(email)) {
-// uniqueRecipients.add(email);
-// return true;
-// }
-// return false;
-// })
-// .map((recipient) => ({
-// name: recipient.name || '',
-// addr: recipient.email,
-// }));
-// msg.setCc(ccRecipients);
-// }
+ try {
+ await this.graphClient.api('/$batch').post({ requests: batchRequests });
+ } catch (error) {
+ console.error('Error during batch update of message read status:', error);
+ throw error;
+ }
+ }
+ public getUserInfo() {
+ return this.withErrorHandler(
+ 'getUserInfo',
+ async () => {
+ const user: User = await this.graphClient
+ .api('/me')
+ .select('id,displayName,userPrincipalName,mail')
+ .get();
-// if (Array.isArray(bcc) && bcc.length > 0) {
-// const bccRecipients = bcc
-// .filter((recipient) => {
-// const email = recipient.email.toLowerCase();
-// if (!uniqueRecipients.has(email)) {
-// uniqueRecipients.add(email);
-// return true;
-// }
-// return false;
-// })
-// .map((recipient) => ({
-// name: recipient.name || '',
-// addr: recipient.email,
-// }));
-// msg.setBcc(bccRecipients);
-// }
+ let photoUrl = '';
+ try {
+ // Requires separate fetching logic
+ } catch (error: unknown) {
+ console.warn(
+ 'Could not fetch user photo:',
+ error instanceof Error ? error.message : 'Unknown error',
+ );
+ }
-// msg.setSubject(subject || '');
-// msg.addMessage({
-// contentType: 'text/html',
-// data: message.trim(),
-// });
+ const info = {
+ address: user.mail || user.userPrincipalName || '',
+ name: user.displayName || '',
+ photo: photoUrl,
+ };
+ console.log({ info });
+ return info;
+ },
+ {},
+ );
+ }
+ public getTokens(code: string) {
+ return this.withErrorHandler(
+ 'getTokens',
+ async () => {
+ const tokens = {
+ accessToken: this.config.auth?.accessToken,
+ refreshToken: this.config.auth?.refreshToken,
+ };
+ return { tokens } as T;
+ },
+ { code },
+ );
+ }
+ public count() {
+ return this.withErrorHandler(
+ 'count',
+ async () => {
+ const userLabels = await this.graphClient.api('/me/mailfolders').get();
-// if (attachments && attachments.length > 0) {
-// for (const attachment of attachments) {
-// msg.addAttachment({
-// filename: attachment.filename,
-// contentType: attachment.contentType,
-// data: attachment.content,
-// });
-// }
-// }
+ if (!userLabels.value) {
+ return [];
+ }
-// return msg.asRaw();
-// };
+ const folders = await Promise.all(
+ userLabels.value.map(async (folder: MailFolder) => {
+ try {
+ const res = await this.graphClient.api(`/me/mailfolders/${folder.id}`).get();
-// const normalizeSearch = (folder: string, q: string) => {
-// if (!q) return '';
-// const searchValue = cleanSearchValue(q);
-// return `contains(subject,'${searchValue}') or contains(body,'${searchValue}')`;
-// };
+ let normalizedLabel = res.displayName || res.id || '';
-// return {
-// // @ts-expect-error, not complete yet
-// get: async (id: string) => {
-// const client = getClient(config.auth?.access_token || '');
-// console.log('get', id);
-// const message: Message = await client.api(`/me/messages/${id}`).get();
-// console.log('message', message);
+ if (res.displayName === 'Inbox' || res.id?.toLowerCase() === 'inbox')
+ normalizedLabel = 'Inbox';
+ else if (res.displayName === 'Sent Items' || res.id?.toLowerCase() === 'sentitems')
+ normalizedLabel = 'Sent';
+ else if (res.displayName === 'Drafts' || res.id?.toLowerCase() === 'drafts')
+ normalizedLabel = 'Drafts';
+ else if (
+ res.displayName === 'Deleted Items' ||
+ res.id?.toLowerCase() === 'deleteditems'
+ )
+ normalizedLabel = 'Bin';
+ else if (res.displayName === 'Archive' || res.id?.toLowerCase() === 'archive')
+ normalizedLabel = 'Archive';
+ else if (res.displayName === 'Junk Email' || res.id?.toLowerCase() === 'junkemail')
+ normalizedLabel = 'Spam';
-// // Get all messages in the conversation using the conversationId
-// // const conversationMessages = await client
-// // .api('/me/messages')
-// // .filter(`conversationId eq '${message.conversationId}'`)
-// // .get();
+ // Use unreadItemCount only for Inbox, use totalItemCount for all other folders
+ const count =
+ res.id?.toLowerCase() === 'inbox'
+ ? Number(res.unreadItemCount)
+ : Number(res.totalItemCount);
-// // console.log('conversationMessages', conversationMessages);
+ return {
+ label: normalizedLabel,
+ count: count ?? undefined,
+ };
+ } catch (error) {
+ console.error(`Error getting counts for folder ${folder.id}:`, error);
+ return {
+ label: folder.displayName || folder.id || '',
+ count: undefined,
+ };
+ }
+ }),
+ );
-// // const messages = [null]
-// return {
-// messages: [
-// {
-// decodedBody: message.body?.content,
-// processedHtml: message.body?.content,
-// title: message.subject,
-// blobUrl: message.body?.content,
-// to: [],
-// receivedOn: message.receivedDateTime
-// ? new Date(message.receivedDateTime).toISOString()
-// : new Date().toISOString(),
-// threadId: message.id,
-// id: message.id,
-// messageId: message.id,
-// subject: message.subject,
-// sender: {
-// email: message.sender?.emailAddress?.address,
-// name: message.sender?.emailAddress?.name || message.sender?.emailAddress?.address,
-// },
-// },
-// ],
-// latest: {
-// to: [],
-// receivedOn: message.receivedDateTime
-// ? new Date(message.receivedDateTime).toISOString()
-// : new Date().toISOString(),
-// threadId: message.id,
-// id: message.id,
-// messageId: message.id,
-// subject: message.subject,
-// sender: {
-// email: message.sender?.emailAddress?.address,
-// name: message.sender?.emailAddress?.name || message.sender?.emailAddress?.address,
-// },
-// },
-// hasUnread: false,
-// totalReplies: 4,
-// };
-// },
+ return folders;
+ },
+ { email: this.config.auth?.email },
+ );
+ }
+ public list(params: {
+ folder: string;
+ query?: string;
+ maxResults?: number;
+ labelIds?: string[];
+ pageToken?: string;
+ }) {
+ const { folder, query: q, maxResults = 100, pageToken } = params;
-// create: async (data: IOutgoingMessage) => {
-// const client = getClient(config.auth?.access_token || '');
-// const rawMessage = await parseOutgoing(data);
-// return client.api('/me/sendMail').post({
-// message: {
-// subject: data.subject,
-// body: {
-// contentType: 'HTML',
-// content: data.message,
-// },
-// toRecipients: data.to.map((r) => ({ emailAddress: { address: r.email } })),
-// ccRecipients: data.cc?.map((r) => ({ emailAddress: { address: r.email } })),
-// bccRecipients: data.bcc?.map((r) => ({ emailAddress: { address: r.email } })),
-// },
-// });
-// },
+ let folderId = this.getOutlookFolderId(folder);
+ if (!folderId) {
+ folderId = folder;
+ }
-// createDraft: async (data: any) => {
-// const client = getClient(config.auth?.access_token || '');
-// return client.api('/me/messages').post({
-// subject: data.subject,
-// body: {
-// contentType: 'HTML',
-// content: data.message,
-// },
-// toRecipients: data.to.map((r: any) => ({ emailAddress: { address: r.email } })),
-// ccRecipients: data.cc?.map((r: any) => ({ emailAddress: { address: r.email } })),
-// bccRecipients: data.bcc?.map((r: any) => ({ emailAddress: { address: r.email } })),
-// });
-// },
-// getUserLabels() {
-// return new Promise((resolve) => resolve([]));
-// },
+ let request = this.graphClient.api(`/me/mailFolders/${folderId}/messages`).top(maxResults);
-// getDraft: async (id: string) => {
-// const client = getClient(config.auth?.access_token || '');
-// const draft = await client.api(`/me/messages/${id}`).get();
-// // return parseMessage(draft);
-// return { id: id };
-// },
+ // if (q) {
+ // request = request.search(`"${q}"`);
+ // }
-// listDrafts: async (q?: string, maxResults = 20, pageToken?: string) => {
-// const client = getClient(config.auth?.access_token || '');
-// const response = await client
-// .api('/me/messages')
-// .filter('isDraft eq true')
-// .top(1)
-// .skip(pageToken ? parseInt(pageToken) : 0)
-// .get();
-// return {
-// drafts: response.value.map(parseMessage),
-// nextPageToken: response['@odata.nextLink']
-// ? (parseInt(pageToken || '0') + maxResults).toString()
-// : undefined,
-// };
-// },
+ request = request.select(
+ 'id,subject,from,toRecipients,ccRecipients,bccRecipients,sentDateTime,receivedDateTime,isRead,internetMessageId,inferenceClassification,categories,parentFolderId',
+ );
-// delete: async (id: string) => {
-// const client = getClient(config.auth?.access_token || '');
-// return client.api(`/me/messages/${id}`).delete();
-// },
+ if (maxResults > 0) {
+ request = request.top(maxResults);
+ }
+ if (pageToken) {
+ console.warn(
+ 'Outlook pagination typically uses @odata.nextLink (full URL). pageToken needs to be handled accordingly.',
+ );
+ }
-// list: async (params: {
-// folder: string;
-// query?: string;
-// maxResults: number;
-// labelIds?: string[];
-// pageToken?: string;
-// }): Promise<{ threads: InitialThread[] }> => {
-// const { folder, query, maxResults = 20, pageToken } = params;
-// const client = getClient(config.auth?.access_token || '');
-// // const searchQuery = query ? normalizeSearch(folder, query) : '';
-// const response = await client
-// .api('/me/messages')
-// // .filter(searchQuery)
-// .top(3)
-// // .skip(pageToken ? parseInt(pageToken.toString()) : 0)
-// .get();
+ // request = request.orderby('receivedDateTime desc');
-// // console.log(response);
+ return this.withErrorHandler(
+ 'list',
+ async () => {
+ const res = await request.get();
-// const threads: InitialThread[] = (response.value as Message[]).map((message) => ({
-// id: message.id ?? '',
-// subject: message.subject,
-// snippet: message.bodyPreview,
-// unread: !message.isRead,
-// date: message.receivedDateTime,
-// }));
+ // console.log(JSON.stringify(res, null, 4));
-// const result = {
-// threads,
-// nextPageToken: response['@odata.nextLink']
-// ? (parseInt(pageToken?.toString() || '0') + maxResults).toString()
-// : undefined,
-// };
+ const messages: Message[] = res.value;
+ const nextPageLink: string | undefined = res['@odata.nextLink'];
-// return result as unknown as { threads: InitialThread[] };
-// },
+ // First parse all messages to get basic info
+ const parsedMessages = await Promise.all(
+ messages.map((msg) => this.parseOutlookMessage(msg)),
+ );
-// count: async () => {
-// // const client = getClient(config.auth?.access_token || '');
-// // const response = await client.api('/me/messages').get();
-// return [];
-// },
+ // Then fetch full content for each message
+ const fullMessages = await Promise.all(
+ messages.map(async (msg, index) => {
+ try {
+ // Get the full message content using the get method
+ const fullMessage = await this.get(msg.id || '');
+ return {
+ ...parsedMessages[index],
+ ...fullMessage.latest,
+ decodedBody: fullMessage.latest.decodedBody || '',
+ };
+ } catch (error) {
+ console.error(`Failed to fetch full message for ${msg.id}:`, error);
+ // If get fails, fall back to basic message info
+ return {
+ ...parsedMessages[index],
+ body: '',
+ processedHtml: '',
+ blobUrl: '',
+ decodedBody: '',
+ attachments: [],
+ };
+ }
+ }),
+ );
-// generateConnectionAuthUrl: (userId: string) => {
-// const params = new URLSearchParams({
-// client_id: process.env.MICROSOFT_CLIENT_ID as string,
-// redirect_uri: process.env.MICROSOFT_REDIRECT_URI as string,
-// response_type: 'code',
-// scope: getScope(),
-// state: userId,
-// });
-// return `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${params.toString()}`;
-// },
+ // Format response according to interface requirements
+ return {
+ threads: messages.map((msg, index) => ({
+ id: msg.id || msg.internetMessageId || '',
+ $raw: {
+ ...msg,
+ ...fullMessages[index],
+ },
+ })),
+ nextPageToken: nextPageLink || null,
+ };
+ },
+ {
+ folder,
+ q,
+ maxResults,
+ _labelIds: params.labelIds,
+ pageToken,
+ email: this.config.auth?.email,
+ },
+ );
+ }
+ private getOutlookFolderId(folderName: string): string | undefined {
+ switch (folderName.toLowerCase()) {
+ case 'inbox':
+ return 'inbox';
+ case 'sent':
+ return 'sentitems';
+ case 'drafts':
+ return 'drafts';
+ case 'bin':
+ case 'trash':
+ return 'deleteditems';
+ case 'archive':
+ return 'archive';
+ case 'junk':
+ case 'spam':
+ return 'junkemail';
+ default:
+ return undefined;
+ }
+ }
+ public get(id: string) {
+ return this.withErrorHandler(
+ 'get',
+ async () => {
+ const message: Message = await this.graphClient
+ .api(`/me/messages/${id}`)
+ .select(
+ 'id,subject,body,from,toRecipients,ccRecipients,bccRecipients,sentDateTime,receivedDateTime,isRead,internetMessageId,inferenceClassification,categories,attachments',
+ )
+ .get();
-// getTokens: async (code: string) => {
-// const params = new URLSearchParams({
-// client_id: process.env.MICROSOFT_CLIENT_ID as string,
-// client_secret: process.env.MICROSOFT_CLIENT_SECRET as string,
-// code,
-// redirect_uri: process.env.MICROSOFT_REDIRECT_URI as string,
-// grant_type: 'authorization_code',
-// });
+ if (!message) {
+ throw new Error('Message not found');
+ }
-// const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
-// method: 'POST',
-// headers: {
-// 'Content-Type': 'application/x-www-form-urlencoded',
-// },
-// body: params.toString(),
-// });
+ const bodyContent = message.body?.content || '';
+ const bodyContentType = message.body?.contentType?.toLowerCase() || 'text';
-// if (!response.ok) {
-// throw new Error('Failed to get tokens');
-// }
+ let decodedBody = '';
+ if (bodyContentType === 'html') {
+ decodedBody = he.decode(bodyContent);
+ } else {
+ decodedBody = he.decode(bodyContent).replace(/\n/g, '
');
+ }
-// const data = await response.json();
-// return {
-// tokens: {
-// access_token: data.access_token,
-// refresh_token: data.refresh_token,
-// expiry_date: Date.now() + data.expires_in * 1000,
-// },
-// };
-// },
-// // @ts-expect-error, fix types
-// getUserInfo: async (tokens: IConfig['auth']) => {
-// if (!tokens?.access_token) throw new Error('No access token provided');
-// const client = getClient(tokens.access_token);
-// const user = await client.api('/me').get();
-// return {
-// address: user.mail || user.userPrincipalName,
-// name: user.displayName,
-// photo: null,
-// };
-// },
+ const attachmentsData = message.attachments || [];
-// getScope,
+ const attachments = await Promise.all(
+ attachmentsData.map(async (att) => {
+ if (!att.id || !att.name || att.size === undefined || att.contentType === undefined) {
+ return null;
+ }
+ const attachmentContent = await this.graphClient
+ .api(`/me/messages/${message.id}/attachments/${att.id}`)
+ .get();
-// markAsRead: async (ids: string[]) => {
-// const client = getClient(config.auth?.access_token || '');
-// await Promise.all(
-// ids.map((id) =>
-// client.api(`/me/messages/${id}`).patch({
-// isRead: true,
-// }),
-// ),
-// );
-// },
+ if (!attachmentContent.contentBytes) {
+ return null;
+ }
-// markAsUnread: async (ids: string[]) => {
-// const client = getClient(config.auth?.access_token || '');
-// await Promise.all(
-// ids.map((id) =>
-// client.api(`/me/messages/${id}`).patch({
-// isRead: false,
-// }),
-// ),
-// );
-// },
+ return {
+ filename: att.name,
+ mimeType: att.contentType ?? 'application/octet-stream',
+ size: att.size,
+ attachmentId: att.id,
+ headers: [],
+ body: attachmentContent.contentBytes,
+ };
+ }),
+ ).then((attachments) => attachments.filter((a): a is NonNullable => a !== null));
-// normalizeIds: (ids: string[]) => ({
-// threadIds: ids,
-// }),
+ const parsedData = this.parseOutlookMessage(message);
-// modifyLabels: async (
-// ids: string[],
-// options: { addLabels: string[]; removeLabels: string[] },
-// ) => {
-// const client = getClient(config.auth?.access_token || '');
-// await Promise.all(
-// ids.map((id) =>
-// client.api(`/me/messages/${id}`).patch({
-// categories: options.addLabels,
-// }),
-// ),
-// );
-// },
+ const fullEmailData = {
+ ...parsedData,
+ body: '',
+ processedHtml: '',
+ blobUrl: '',
+ decodedBody: decodedBody,
+ attachments,
+ };
-// getAttachment: async (messageId: string, attachmentId: string) => {
-// const client = getClient(config.auth?.access_token || '');
-// const attachment = await client
-// .api(`/me/messages/${messageId}/attachments/${attachmentId}`)
-// .get();
-// return attachment.contentBytes;
-// },
-// };
-// };
+ return {
+ labels: parsedData.tags,
+ messages: [fullEmailData],
+ latest: fullEmailData,
+ hasUnread: parsedData.unread,
+ totalReplies: 1,
+ };
+ },
+ { id, email: this.config.auth?.email },
+ );
+ }
+ public create(data: IOutgoingMessage) {
+ return this.withErrorHandler(
+ 'create',
+ async () => {
+ const messagePayload = await this.parseOutgoingOutlook(data);
+
+ const res = await this.graphClient.api('/me/sendMail').post({
+ message: messagePayload,
+ saveToSentItems: true,
+ });
+
+ return res;
+ },
+ { data, email: this.config.auth?.email },
+ );
+ }
+ public delete(id: string) {
+ return this.withErrorHandler(
+ 'delete',
+ async () => {
+ await this.graphClient.api(`/me/messages/${id}`).delete();
+ },
+ { id },
+ );
+ }
+ public normalizeIds(ids: string[]) {
+ return this.withSyncErrorHandler(
+ 'normalizeIds',
+ () => {
+ const messageIds: string[] = ids.map((id) =>
+ id.startsWith('thread:') ? id.substring(7) : id,
+ );
+ return { threadIds: messageIds }; // Renamed from threadIds to messageIds conceptually
+ },
+ { ids },
+ );
+ }
+ public modifyLabels(
+ messageIds: string[],
+ options: { addLabels: string[]; removeLabels: string[] },
+ ) {
+ return this.withErrorHandler(
+ 'modifyLabels',
+ async () => {
+ await this.modifyMessageLabelsOrFolders(
+ messageIds,
+ options.addLabels,
+ options.removeLabels,
+ );
+ },
+ { messageIds, options },
+ );
+ }
+ private async modifyMessageLabelsOrFolders(
+ messageIds: string[],
+ addItems: string[],
+ removeItems: string[],
+ ) {
+ if (messageIds.length === 0) {
+ return;
+ }
+ const batchRequests = messageIds.map((id, index) => {
+ const patchBody = {};
+
+ if (addItems.length > 0 || removeItems.length > 0) {
+ console.warn(
+ `Modifying categories (${addItems.join(',')}, ${removeItems.join(',')}) on message ${id} is not fully implemented.`,
+ );
+ }
+
+ if (!addItems[0]) {
+ console.warn('No addItems');
+ return;
+ }
+
+ let moveToFolderId: string | undefined;
+ if (addItems.length > 0 && this.getOutlookFolderId(addItems[0])) {
+ moveToFolderId = this.getOutlookFolderId(addItems[0]) || addItems[0];
+ console.warn(
+ `Attempting to move message ${id} to folder ${moveToFolderId}. This is a move operation, not adding a label.`,
+ );
+ return {
+ id: `${index}`,
+ method: 'POST',
+ url: `/me/messages/${id}/move`,
+ body: { destinationId: moveToFolderId },
+ headers: { 'Content-Type': 'application/json' },
+ };
+ }
+ return {
+ id: `${index}`,
+ method: 'PATCH',
+ url: `/me/messages/${id}`,
+ body: patchBody,
+ headers: { 'Content-Type': 'application/json' },
+ };
+ });
+
+ const validBatchRequests = batchRequests
+ .filter((req) => typeof req !== 'undefined')
+ .filter((req) => Object.keys(req.body).length > 0 || req.method === 'POST');
+
+ if (validBatchRequests.length === 0) {
+ console.warn('No valid batch requests generated for modifyMessageLabelsOrFolders.');
+ return;
+ }
+
+ try {
+ await this.graphClient.api('/$batch').post({ requests: validBatchRequests });
+ } catch (error) {
+ console.error('Error during batch modification of messages:', error);
+ throw error;
+ }
+ }
+ public sendDraft(draftId: string, data: IOutgoingMessage) {
+ return this.withErrorHandler(
+ 'sendDraft',
+ async () => {
+ await this.graphClient.api(`/me/drafts/${draftId}/send`).post({});
+ },
+ { draftId, data },
+ );
+ }
+ public getDraft(draftId: string) {
+ return this.withErrorHandler(
+ 'getDraft',
+ async () => {
+ const draftMessage: Message = await this.graphClient
+ .api(`/me/messages/${draftId}`) // Drafts are messages in the drafts folder
+ .select('id,subject,body,from,toRecipients,ccRecipients,bccRecipients')
+ .get();
+
+ if (!draftMessage) {
+ throw new Error('Draft not found');
+ }
+
+ const parsedDraft = this.parseOutlookDraft(draftMessage);
+ if (!parsedDraft) {
+ throw new Error('Failed to parse draft');
+ }
+
+ return parsedDraft;
+ },
+ { draftId },
+ );
+ }
+ public listDrafts(params: { q?: string; maxResults?: number; pageToken?: string }) {
+ const { q, maxResults = 20, pageToken } = params;
+ return this.withErrorHandler(
+ 'listDrafts',
+ async () => {
+ let request = this.graphClient.api('/me/mailfolders/drafts/messages');
+
+ // if (q) {
+ // request = request.search(`"${q}"`);
+ // }
+
+ request = request.select(
+ 'id,subject,from,toRecipients,ccRecipients,bccRecipients,sentDateTime,receivedDateTime,isRead,internetMessageId',
+ );
+ // request = request.orderby('receivedDateTime desc');
+ request = request.top(maxResults);
+
+ if (pageToken) {
+ console.warn(
+ 'Outlook pagination typically uses @odata.nextLink (full URL). pageToken needs to be handled accordingly.',
+ );
+ }
+
+ const res = await request.get();
+
+ const draftMessages: Message[] = res.value;
+ const nextPageLink: string | undefined = res['@odata.nextLink'];
+
+ const drafts = await Promise.all(
+ draftMessages.map(async (message) => {
+ if (!message.id) return null;
+ try {
+ const parsed = this.parseOutlookMessage(message);
+ return {
+ ...parsed,
+ id: message.id,
+ threadId: message.conversationId || message.id,
+ receivedOn: message.receivedDateTime || new Date().toISOString(),
+ };
+ } catch (error) {
+ console.error('Error parsing draft message:', error);
+ return null;
+ }
+ }),
+ );
+
+ const sortedDrafts = drafts
+ .filter((draft) => draft !== null)
+ .sort((a, b) => {
+ const dateA = new Date(a?.receivedOn || new Date()).getTime();
+ const dateB = new Date(b?.receivedOn || new Date()).getTime();
+ return dateB - dateA;
+ });
+
+ return {
+ threads: sortedDrafts.map((draft) => ({
+ id: draft.id,
+ $raw: draft,
+ })),
+ nextPageToken: nextPageLink || null,
+ };
+ },
+ { q, maxResults, pageToken },
+ );
+ }
+ public createDraft(data: CreateDraftData) {
+ return this.withErrorHandler(
+ 'createDraft',
+ async () => {
+ const message = await sanitizeTipTapHtml(data.message);
+
+ const toRecipients = Array.isArray(data.to) ? data.to : data.to.split(', ');
+
+ const outlookMessage: Message = {
+ subject: data.subject,
+ body: {
+ contentType: 'html',
+ content: message || '',
+ },
+ toRecipients: toRecipients.map((recipient) => ({
+ emailAddress: {
+ address: typeof recipient === 'string' ? recipient : recipient.email,
+ name: typeof recipient === 'string' ? undefined : recipient.name || undefined,
+ },
+ })),
+ };
+
+ if (data.cc) {
+ const ccRecipients = Array.isArray(data.cc) ? data.cc : data.cc.split(', ');
+ outlookMessage.ccRecipients = ccRecipients.map((recipient) => ({
+ emailAddress: {
+ address: typeof recipient === 'string' ? recipient : recipient.email,
+ name: typeof recipient === 'string' ? undefined : recipient.name || undefined,
+ },
+ }));
+ }
+
+ if (data.bcc) {
+ const bccRecipients = Array.isArray(data.bcc) ? data.bcc : data.bcc.split(', ');
+ outlookMessage.bccRecipients = bccRecipients.map((recipient) => ({
+ emailAddress: {
+ address: typeof recipient === 'string' ? recipient : recipient.email,
+ name: typeof recipient === 'string' ? undefined : recipient.name || undefined,
+ },
+ }));
+ }
+
+ if (data.attachments && data.attachments.length > 0) {
+ outlookMessage.attachments = await Promise.all(
+ data.attachments.map(async (file) => {
+ const arrayBuffer = await file.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+ const base64Content = buffer.toString('base64');
+
+ return {
+ '@odata.type': '#microsoft.graph.fileAttachment',
+ name: file.name,
+ contentType: file.type || 'application/octet-stream',
+ contentBytes: base64Content,
+ };
+ }),
+ );
+ }
+
+ let res;
+
+ if (data.id) {
+ try {
+ res = await this.graphClient
+ .api(`/me/mailfolders/drafts/messages/${data.id}`)
+ .patch(outlookMessage);
+ } catch (error) {
+ console.warn(`Failed to update draft ${data.id}, creating a new one`, error);
+ try {
+ await this.graphClient.api(`/me/mailfolders/drafts/messages/${data.id}`).delete();
+ } catch (deleteError) {
+ console.error(`Failed to delete draft ${data.id}`, deleteError);
+ }
+
+ res = await this.graphClient
+ .api('/me/mailfolders/drafts/messages')
+ .post(outlookMessage);
+ }
+ } else {
+ res = await this.graphClient.api('/me/mailfolders/drafts/messages').post(outlookMessage);
+ }
+
+ return res;
+ },
+ { data },
+ );
+ }
+ public async getUserLabels() {
+ console.warn(
+ 'getUserLabels maps to Outlook Categories and Mail Folders, which have different APIs.',
+ );
+
+ try {
+ const categories: Category[] = (
+ await this.graphClient.api('/me/outlook/masterCategories').get()
+ ).value;
+ const folders: MailFolder[] = (await this.graphClient.api('/me/mailfolders').get()).value;
+
+ const mappedCategories: Label[] = categories.map((cat: Category) => ({
+ id: cat.id || cat.displayName || '',
+ name: cat.displayName || '',
+ type: 'category', // Indicate these are categories
+ color: {
+ backgroundColor: cat.color || '', // Graph category color is a string enum, not hex
+ textColor: '', // Outlook categories don't have separate text color in API
+ },
+ }));
+
+ const mappedFolders: Label[] = folders.map((folder) => ({
+ id: folder.id || '',
+ name: folder.displayName || '',
+ type: 'user', // Differentiate system vs user folders
+ color: {
+ backgroundColor: '', // Outlook folders don't have colors via API
+ textColor: '',
+ },
+ }));
+
+ return [...mappedCategories, ...mappedFolders];
+ } catch (error) {
+ console.error('Error fetching Outlook categories or folders:', error);
+ return [];
+ }
+ }
+ public async getLabel(labelId: string): Promise