@realtimex/email-automator 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/server.ts +4 -2
- package/api/src/config/index.ts +11 -9
- package/bin/email-automator.js +4 -24
- package/dist/api/server.js +109 -0
- package/dist/api/src/config/index.js +89 -0
- package/dist/api/src/middleware/auth.js +119 -0
- package/dist/api/src/middleware/errorHandler.js +78 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +57 -0
- package/dist/api/src/middleware/validation.js +111 -0
- package/dist/api/src/routes/actions.js +173 -0
- package/dist/api/src/routes/auth.js +106 -0
- package/dist/api/src/routes/emails.js +100 -0
- package/dist/api/src/routes/health.js +33 -0
- package/dist/api/src/routes/index.js +19 -0
- package/dist/api/src/routes/migrate.js +61 -0
- package/dist/api/src/routes/rules.js +104 -0
- package/dist/api/src/routes/settings.js +178 -0
- package/dist/api/src/routes/sync.js +118 -0
- package/dist/api/src/services/eventLogger.js +41 -0
- package/dist/api/src/services/gmail.js +350 -0
- package/dist/api/src/services/intelligence.js +243 -0
- package/dist/api/src/services/microsoft.js +256 -0
- package/dist/api/src/services/processor.js +503 -0
- package/dist/api/src/services/scheduler.js +210 -0
- package/dist/api/src/services/supabase.js +59 -0
- package/dist/api/src/utils/contentCleaner.js +94 -0
- package/dist/api/src/utils/crypto.js +68 -0
- package/dist/api/src/utils/logger.js +119 -0
- package/package.json +5 -4
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import Instructor from '@instructor-ai/instructor';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { config } from '../config/index.js';
|
|
5
|
+
import { createLogger } from '../utils/logger.js';
|
|
6
|
+
import { ContentCleaner } from '../utils/contentCleaner.js';
|
|
7
|
+
const logger = createLogger('Intelligence');
|
|
8
|
+
// Define the schema for email analysis
|
|
9
|
+
export const EmailAnalysisSchema = z.object({
|
|
10
|
+
summary: z.string().describe('A brief summary of the email content'),
|
|
11
|
+
category: z.enum(['spam', 'newsletter', 'promotional', 'transactional', 'social', 'support', 'client', 'internal', 'personal', 'other'])
|
|
12
|
+
.describe('The category of the email'),
|
|
13
|
+
sentiment: z.enum(['Positive', 'Neutral', 'Negative'])
|
|
14
|
+
.describe('The emotional tone of the email'),
|
|
15
|
+
is_useless: z.boolean()
|
|
16
|
+
.describe('Whether the email is considered useless (spam, newsletter, etc.)'),
|
|
17
|
+
suggested_actions: z.array(z.enum(['none', 'delete', 'archive', 'reply', 'flag']))
|
|
18
|
+
.describe('The recommended next actions (e.g. ["reply", "archive"])'),
|
|
19
|
+
draft_response: z.string().optional()
|
|
20
|
+
.describe('A suggested draft response if the action is reply'),
|
|
21
|
+
priority: z.enum(['High', 'Medium', 'Low'])
|
|
22
|
+
.describe('The urgency of the email'),
|
|
23
|
+
key_points: z.array(z.string()).optional()
|
|
24
|
+
.describe('Key points extracted from the email'),
|
|
25
|
+
action_items: z.array(z.string()).optional()
|
|
26
|
+
.describe('Action items mentioned in the email'),
|
|
27
|
+
});
|
|
28
|
+
export class IntelligenceService {
|
|
29
|
+
client;
|
|
30
|
+
model;
|
|
31
|
+
isConfigured = false;
|
|
32
|
+
constructor(overrides) {
|
|
33
|
+
const apiKey = overrides?.apiKey || config.llm.apiKey;
|
|
34
|
+
const baseUrl = overrides?.baseUrl || config.llm.baseUrl;
|
|
35
|
+
this.model = overrides?.model || config.llm.model;
|
|
36
|
+
// Allow local LLM servers (LM Studio, Ollama) or custom endpoints that don't need API keys
|
|
37
|
+
// We assume any custom baseUrl might be a local/private instance.
|
|
38
|
+
const isCustomEndpoint = baseUrl && !baseUrl.includes('api.openai.com');
|
|
39
|
+
if (!apiKey && !isCustomEndpoint) {
|
|
40
|
+
logger.warn('LLM_API_KEY is missing and no custom LLM endpoint configured. AI analysis will not work.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const oai = new OpenAI({
|
|
45
|
+
apiKey: apiKey || 'not-needed-for-local', // Placeholder for local LLMs
|
|
46
|
+
baseURL: baseUrl,
|
|
47
|
+
});
|
|
48
|
+
this.client = Instructor({
|
|
49
|
+
client: oai,
|
|
50
|
+
mode: 'MD_JSON',
|
|
51
|
+
});
|
|
52
|
+
this.isConfigured = true;
|
|
53
|
+
logger.info('Intelligence service initialized', { model: this.model, baseUrl: baseUrl || 'default' });
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error('[Intelligence] Init failed:', error);
|
|
57
|
+
logger.error('Failed to initialize Intelligence service', error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
isReady() {
|
|
61
|
+
const ready = this.isConfigured && !!this.client;
|
|
62
|
+
if (!ready)
|
|
63
|
+
console.log('[Intelligence] isReady check failed:', { isConfigured: this.isConfigured, hasClient: !!this.client });
|
|
64
|
+
return ready;
|
|
65
|
+
}
|
|
66
|
+
async analyzeEmail(content, context, eventLogger, emailId) {
|
|
67
|
+
console.log('[Intelligence] analyzeEmail called for:', context.subject);
|
|
68
|
+
if (!this.isReady()) {
|
|
69
|
+
console.log('[Intelligence] Not ready, skipping');
|
|
70
|
+
logger.warn('Intelligence service not ready, skipping analysis');
|
|
71
|
+
if (eventLogger) {
|
|
72
|
+
await eventLogger.info('Skipped', 'AI Analysis skipped: Model not configured. Please check settings.', undefined, emailId);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
// 1. Prepare Content and Signals
|
|
77
|
+
const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
|
|
78
|
+
const metadataSignals = [];
|
|
79
|
+
if (context.metadata?.listUnsubscribe)
|
|
80
|
+
metadataSignals.push('- Contains Unsubscribe header (High signal for Newsletter/Promo)');
|
|
81
|
+
if (context.metadata?.autoSubmitted && context.metadata.autoSubmitted !== 'no')
|
|
82
|
+
metadataSignals.push(`- Auto-Submitted: ${context.metadata.autoSubmitted}`);
|
|
83
|
+
if (context.metadata?.importance)
|
|
84
|
+
metadataSignals.push(`- Sender Priority/Importance: ${context.metadata.importance}`);
|
|
85
|
+
if (context.metadata?.mailer)
|
|
86
|
+
metadataSignals.push(`- Sent via: ${context.metadata.mailer}`);
|
|
87
|
+
const systemPrompt = `You are an AI Email Assistant. Your task is to analyze the provided email and extract structured information as JSON.
|
|
88
|
+
Do NOT include any greetings, chatter, or special tokens like <|channel|> in your output.
|
|
89
|
+
Return ONLY a valid JSON object.
|
|
90
|
+
|
|
91
|
+
Definitions for Categories:
|
|
92
|
+
- "important": Work-related, urgent, from known contacts
|
|
93
|
+
- "promotional": Marketing, sales, discounts
|
|
94
|
+
- "transactional": Receipts, shipping, confirmations
|
|
95
|
+
- "social": LinkedIn, friends, social updates
|
|
96
|
+
- "newsletter": Subscribed content
|
|
97
|
+
- "spam": Junk, suspicious
|
|
98
|
+
|
|
99
|
+
Context:
|
|
100
|
+
- Current Date: ${new Date().toISOString()}
|
|
101
|
+
- Subject: ${context.subject}
|
|
102
|
+
- From: ${context.sender}
|
|
103
|
+
- Date: ${context.date}
|
|
104
|
+
${metadataSignals.length > 0 ? `\nMetadata Signals:\n${metadataSignals.join('\n')}` : ''}
|
|
105
|
+
${context.userPreferences?.autoTrashSpam ? '- User has auto-trash spam enabled' : ''}
|
|
106
|
+
${context.userPreferences?.smartDrafts ? '- User wants draft responses for important emails' : ''}
|
|
107
|
+
|
|
108
|
+
REQUIRED JSON STRUCTURE:
|
|
109
|
+
{
|
|
110
|
+
"summary": "string",
|
|
111
|
+
"category": "spam|newsletter|promotional|transactional|social|support|client|internal|personal|other",
|
|
112
|
+
"sentiment": "Positive|Neutral|Negative",
|
|
113
|
+
"is_useless": boolean,
|
|
114
|
+
"suggested_actions": ["none"|"delete"|"archive"|"reply"|"flag"],
|
|
115
|
+
"draft_response": "string (optional)",
|
|
116
|
+
"priority": "High|Medium|Low",
|
|
117
|
+
"key_points": ["string"],
|
|
118
|
+
"action_items": ["string"]
|
|
119
|
+
}`;
|
|
120
|
+
// 2. Log Thinking Phase
|
|
121
|
+
if (eventLogger) {
|
|
122
|
+
console.log('[Intelligence] Logging "Thinking" event');
|
|
123
|
+
try {
|
|
124
|
+
await eventLogger.info('Thinking', `Analyzing email: ${context.subject}`, {
|
|
125
|
+
model: this.model,
|
|
126
|
+
system_prompt: systemPrompt,
|
|
127
|
+
content_preview: cleanedContent
|
|
128
|
+
}, emailId);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
console.error('[Intelligence] Failed to log thinking event:', err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
let rawResponse = '';
|
|
135
|
+
try {
|
|
136
|
+
// Using raw completion call to handle garbage characters and strip tokens manually
|
|
137
|
+
const response = await this.client.client.chat.completions.create({
|
|
138
|
+
model: this.model,
|
|
139
|
+
messages: [
|
|
140
|
+
{ role: 'system', content: systemPrompt },
|
|
141
|
+
{ role: 'user', content: cleanedContent || '[Empty email body]' },
|
|
142
|
+
],
|
|
143
|
+
temperature: 0.1,
|
|
144
|
+
});
|
|
145
|
+
rawResponse = response.choices[0]?.message?.content || '';
|
|
146
|
+
console.log('[Intelligence] Raw LLM Response received (length:', rawResponse.length, ')');
|
|
147
|
+
// Clean the response: Find first '{' and last '}'
|
|
148
|
+
let jsonStr = rawResponse.trim();
|
|
149
|
+
const startIdx = jsonStr.indexOf('{');
|
|
150
|
+
const endIdx = jsonStr.lastIndexOf('}');
|
|
151
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
152
|
+
throw new Error('Response did not contain a valid JSON object (missing curly braces)');
|
|
153
|
+
}
|
|
154
|
+
jsonStr = jsonStr.substring(startIdx, endIdx + 1);
|
|
155
|
+
const parsed = JSON.parse(jsonStr);
|
|
156
|
+
const validated = EmailAnalysisSchema.parse(parsed);
|
|
157
|
+
logger.debug('Email analyzed', {
|
|
158
|
+
category: validated.category,
|
|
159
|
+
actions: validated.suggested_actions,
|
|
160
|
+
});
|
|
161
|
+
if (eventLogger && emailId) {
|
|
162
|
+
await eventLogger.analysis('Decided', emailId, {
|
|
163
|
+
...validated,
|
|
164
|
+
_raw_response: rawResponse
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return validated;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
console.error('[Intelligence] AI Analysis failed:', error);
|
|
171
|
+
if (eventLogger) {
|
|
172
|
+
await eventLogger.error('Error', {
|
|
173
|
+
error: error instanceof Error ? error.message : String(error),
|
|
174
|
+
raw_response: rawResponse || 'No response received from LLM'
|
|
175
|
+
}, emailId);
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async generateDraftReply(originalEmail, instructions) {
|
|
181
|
+
if (!this.isReady()) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const response = await this.client.chat.completions.create({
|
|
186
|
+
model: this.model,
|
|
187
|
+
messages: [
|
|
188
|
+
{
|
|
189
|
+
role: 'system',
|
|
190
|
+
content: `You are a professional email assistant. Generate a polite and appropriate reply to the email.
|
|
191
|
+
${instructions ? `Additional instructions: ${instructions}` : ''}
|
|
192
|
+
Keep the response concise and professional.`,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
role: 'user',
|
|
196
|
+
content: `Original email from ${originalEmail.sender}:
|
|
197
|
+
Subject: ${originalEmail.subject}
|
|
198
|
+
|
|
199
|
+
${originalEmail.body}
|
|
200
|
+
|
|
201
|
+
Please write a reply.`,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
return response.choices[0]?.message?.content || null;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
logger.error('Draft generation failed', error);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async testConnection() {
|
|
213
|
+
if (!this.isReady()) {
|
|
214
|
+
return { success: false, message: 'Intelligence service not initialized. Check your API Key.' };
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
await this.client.chat.completions.create({
|
|
218
|
+
model: this.model,
|
|
219
|
+
messages: [{ role: 'user', content: 'Say "Connection Successful"' }],
|
|
220
|
+
max_tokens: 5,
|
|
221
|
+
});
|
|
222
|
+
return { success: true, message: 'Connection successful!' };
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
logger.error('Connection test failed', error);
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Singleton instance with default config
|
|
234
|
+
let defaultInstance = null;
|
|
235
|
+
export function getIntelligenceService(overrides) {
|
|
236
|
+
if (overrides) {
|
|
237
|
+
return new IntelligenceService(overrides);
|
|
238
|
+
}
|
|
239
|
+
if (!defaultInstance) {
|
|
240
|
+
defaultInstance = new IntelligenceService();
|
|
241
|
+
}
|
|
242
|
+
return defaultInstance;
|
|
243
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import * as msal from '@azure/msal-node';
|
|
2
|
+
import { config } from '../config/index.js';
|
|
3
|
+
import { createLogger } from '../utils/logger.js';
|
|
4
|
+
const logger = createLogger('MicrosoftService');
|
|
5
|
+
const GRAPH_SCOPES = [
|
|
6
|
+
'https://graph.microsoft.com/Mail.Read',
|
|
7
|
+
'https://graph.microsoft.com/Mail.ReadWrite',
|
|
8
|
+
'https://graph.microsoft.com/User.Read',
|
|
9
|
+
];
|
|
10
|
+
export class MicrosoftService {
|
|
11
|
+
pca;
|
|
12
|
+
cca = null;
|
|
13
|
+
constructor() {
|
|
14
|
+
const publicConfig = {
|
|
15
|
+
auth: {
|
|
16
|
+
clientId: config.microsoft.clientId,
|
|
17
|
+
authority: `https://login.microsoftonline.com/${config.microsoft.tenantId}`,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
this.pca = new msal.PublicClientApplication(publicConfig);
|
|
21
|
+
// Confidential client for server-side token refresh
|
|
22
|
+
if (config.microsoft.clientSecret) {
|
|
23
|
+
const confidentialConfig = {
|
|
24
|
+
auth: {
|
|
25
|
+
clientId: config.microsoft.clientId,
|
|
26
|
+
authority: `https://login.microsoftonline.com/${config.microsoft.tenantId}`,
|
|
27
|
+
clientSecret: config.microsoft.clientSecret,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
this.cca = new msal.ConfidentialClientApplication(confidentialConfig);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async initiateDeviceCodeFlow() {
|
|
34
|
+
const deviceCodeRequest = {
|
|
35
|
+
scopes: GRAPH_SCOPES,
|
|
36
|
+
deviceCodeCallback: (response) => {
|
|
37
|
+
logger.info('Device code received', { userCode: response.userCode });
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const response = await this.pca.acquireTokenByDeviceCode(deviceCodeRequest);
|
|
41
|
+
// The device code flow returns tokens directly after user completes auth
|
|
42
|
+
// For now, we return the device code info for the frontend to display
|
|
43
|
+
return {
|
|
44
|
+
userCode: '',
|
|
45
|
+
verificationUri: 'https://microsoft.com/devicelogin',
|
|
46
|
+
message: 'Please visit https://microsoft.com/devicelogin and enter the code shown',
|
|
47
|
+
expiresIn: 900,
|
|
48
|
+
interval: 5,
|
|
49
|
+
deviceCode: '',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async acquireTokenByDeviceCode(deviceCodeCallback) {
|
|
53
|
+
try {
|
|
54
|
+
const response = await this.pca.acquireTokenByDeviceCode({
|
|
55
|
+
scopes: GRAPH_SCOPES,
|
|
56
|
+
deviceCodeCallback,
|
|
57
|
+
});
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
logger.error('Device code flow failed', error);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async saveAccount(supabase, userId, emailAddress, authResult) {
|
|
66
|
+
const { data, error } = await supabase
|
|
67
|
+
.from('email_accounts')
|
|
68
|
+
.upsert({
|
|
69
|
+
user_id: userId,
|
|
70
|
+
email_address: emailAddress,
|
|
71
|
+
provider: 'outlook',
|
|
72
|
+
access_token: authResult.accessToken,
|
|
73
|
+
refresh_token: null, // MSAL handles token cache internally
|
|
74
|
+
token_expires_at: authResult.expiresOn?.toISOString() || null,
|
|
75
|
+
scopes: authResult.scopes,
|
|
76
|
+
is_active: true,
|
|
77
|
+
updated_at: new Date().toISOString(),
|
|
78
|
+
}, { onConflict: 'user_id, email_address' })
|
|
79
|
+
.select()
|
|
80
|
+
.single();
|
|
81
|
+
if (error)
|
|
82
|
+
throw error;
|
|
83
|
+
return data;
|
|
84
|
+
}
|
|
85
|
+
async refreshTokenIfNeeded(supabase, account) {
|
|
86
|
+
if (!account.token_expires_at)
|
|
87
|
+
return account;
|
|
88
|
+
const expiresAt = new Date(account.token_expires_at).getTime();
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const bufferMs = 5 * 60 * 1000;
|
|
91
|
+
if (expiresAt > now + bufferMs) {
|
|
92
|
+
return account;
|
|
93
|
+
}
|
|
94
|
+
logger.info('Refreshing Microsoft token', { accountId: account.id });
|
|
95
|
+
// For Microsoft, we need the CCA with client secret for refresh
|
|
96
|
+
if (!this.cca) {
|
|
97
|
+
throw new Error('Microsoft client secret not configured for token refresh');
|
|
98
|
+
}
|
|
99
|
+
// In production, you'd use refresh tokens stored in a token cache
|
|
100
|
+
// This is a simplified implementation
|
|
101
|
+
throw new Error('Microsoft token refresh requires re-authentication');
|
|
102
|
+
}
|
|
103
|
+
async fetchMessages(account, options = {}) {
|
|
104
|
+
const accessToken = account.access_token || '';
|
|
105
|
+
const { top = 20, skip = 0, filter } = options;
|
|
106
|
+
let url = `https://graph.microsoft.com/v1.0/me/messages?$top=${top}&$skip=${skip}&$orderby=receivedDateTime desc&$select=id,conversationId,subject,from,toRecipients,receivedDateTime,body,bodyPreview,importance`;
|
|
107
|
+
if (filter) {
|
|
108
|
+
url += `&$filter=${encodeURIComponent(filter)}`;
|
|
109
|
+
}
|
|
110
|
+
const response = await fetch(url, {
|
|
111
|
+
headers: {
|
|
112
|
+
Authorization: `Bearer ${accessToken}`,
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const error = await response.text();
|
|
118
|
+
logger.error('Failed to fetch Outlook messages', new Error(error));
|
|
119
|
+
throw new Error('Failed to fetch messages from Outlook');
|
|
120
|
+
}
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
const messages = (data.value || []).map((msg) => ({
|
|
123
|
+
id: msg.id,
|
|
124
|
+
conversationId: msg.conversationId,
|
|
125
|
+
subject: msg.subject || 'No Subject',
|
|
126
|
+
sender: msg.from?.emailAddress?.address || 'Unknown',
|
|
127
|
+
recipient: msg.toRecipients?.[0]?.emailAddress?.address || '',
|
|
128
|
+
date: msg.receivedDateTime,
|
|
129
|
+
body: msg.body?.content || '',
|
|
130
|
+
snippet: msg.bodyPreview || '',
|
|
131
|
+
headers: {
|
|
132
|
+
importance: msg.importance,
|
|
133
|
+
}
|
|
134
|
+
}));
|
|
135
|
+
return {
|
|
136
|
+
messages,
|
|
137
|
+
hasMore: !!data['@odata.nextLink'],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async trashMessage(account, messageId) {
|
|
141
|
+
const accessToken = account.access_token || '';
|
|
142
|
+
const response = await fetch(`https://graph.microsoft.com/v1.0/me/messages/${messageId}/move`, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: {
|
|
145
|
+
Authorization: `Bearer ${accessToken}`,
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify({ destinationId: 'deleteditems' }),
|
|
149
|
+
});
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
throw new Error('Failed to trash message');
|
|
152
|
+
}
|
|
153
|
+
logger.debug('Outlook message trashed', { messageId });
|
|
154
|
+
}
|
|
155
|
+
async archiveMessage(account, messageId) {
|
|
156
|
+
const accessToken = account.access_token || '';
|
|
157
|
+
const response = await fetch(`https://graph.microsoft.com/v1.0/me/messages/${messageId}/move`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
Authorization: `Bearer ${accessToken}`,
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({ destinationId: 'archive' }),
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
throw new Error('Failed to archive message');
|
|
167
|
+
}
|
|
168
|
+
logger.debug('Outlook message archived', { messageId });
|
|
169
|
+
}
|
|
170
|
+
async markAsRead(account, messageId) {
|
|
171
|
+
const accessToken = account.access_token || '';
|
|
172
|
+
await fetch(`https://graph.microsoft.com/v1.0/me/messages/${messageId}`, {
|
|
173
|
+
method: 'PATCH',
|
|
174
|
+
headers: {
|
|
175
|
+
Authorization: `Bearer ${accessToken}`,
|
|
176
|
+
'Content-Type': 'application/json',
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify({ isRead: true }),
|
|
179
|
+
});
|
|
180
|
+
logger.debug('Outlook message marked as read', { messageId });
|
|
181
|
+
}
|
|
182
|
+
async flagMessage(account, messageId) {
|
|
183
|
+
const accessToken = account.access_token || '';
|
|
184
|
+
await fetch(`https://graph.microsoft.com/v1.0/me/messages/${messageId}`, {
|
|
185
|
+
method: 'PATCH',
|
|
186
|
+
headers: {
|
|
187
|
+
Authorization: `Bearer ${accessToken}`,
|
|
188
|
+
'Content-Type': 'application/json',
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({ flag: { flagStatus: 'flagged' } }),
|
|
191
|
+
});
|
|
192
|
+
logger.debug('Outlook message flagged', { messageId });
|
|
193
|
+
}
|
|
194
|
+
async createDraft(account, originalMessageId, replyContent) {
|
|
195
|
+
const accessToken = account.access_token || '';
|
|
196
|
+
// Get original message
|
|
197
|
+
const originalResponse = await fetch(`https://graph.microsoft.com/v1.0/me/messages/${originalMessageId}`, {
|
|
198
|
+
headers: {
|
|
199
|
+
Authorization: `Bearer ${accessToken}`,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const original = await originalResponse.json();
|
|
203
|
+
// Create reply draft
|
|
204
|
+
const response = await fetch(`https://graph.microsoft.com/v1.0/me/messages/${originalMessageId}/createReply`, {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers: {
|
|
207
|
+
Authorization: `Bearer ${accessToken}`,
|
|
208
|
+
'Content-Type': 'application/json',
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
throw new Error('Failed to create reply draft');
|
|
213
|
+
}
|
|
214
|
+
const draft = await response.json();
|
|
215
|
+
// Update draft with content
|
|
216
|
+
await fetch(`https://graph.microsoft.com/v1.0/me/messages/${draft.id}`, {
|
|
217
|
+
method: 'PATCH',
|
|
218
|
+
headers: {
|
|
219
|
+
Authorization: `Bearer ${accessToken}`,
|
|
220
|
+
'Content-Type': 'application/json',
|
|
221
|
+
},
|
|
222
|
+
body: JSON.stringify({
|
|
223
|
+
body: {
|
|
224
|
+
contentType: 'text',
|
|
225
|
+
content: replyContent,
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
logger.debug('Outlook draft created', { draftId: draft.id });
|
|
230
|
+
return draft.id;
|
|
231
|
+
}
|
|
232
|
+
async getProfile(account) {
|
|
233
|
+
const accessToken = account.access_token || '';
|
|
234
|
+
const response = await fetch('https://graph.microsoft.com/v1.0/me', {
|
|
235
|
+
headers: {
|
|
236
|
+
Authorization: `Bearer ${accessToken}`,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
throw new Error('Failed to get profile');
|
|
241
|
+
}
|
|
242
|
+
const profile = await response.json();
|
|
243
|
+
return {
|
|
244
|
+
emailAddress: profile.mail || profile.userPrincipalName || '',
|
|
245
|
+
displayName: profile.displayName || '',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Singleton
|
|
250
|
+
let instance = null;
|
|
251
|
+
export function getMicrosoftService() {
|
|
252
|
+
if (!instance) {
|
|
253
|
+
instance = new MicrosoftService();
|
|
254
|
+
}
|
|
255
|
+
return instance;
|
|
256
|
+
}
|