@realtimex/email-automator 2.10.5 → 2.10.7

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.
@@ -11,6 +11,9 @@ const GRAPH_SCOPES = [
11
11
  'https://graph.microsoft.com/Mail.Read',
12
12
  'https://graph.microsoft.com/Mail.ReadWrite',
13
13
  'https://graph.microsoft.com/User.Read',
14
+ 'offline_access',
15
+ 'openid',
16
+ 'profile',
14
17
  ];
15
18
 
16
19
  export interface OutlookMessage {
@@ -97,6 +100,9 @@ export class MicrosoftService {
97
100
  emailAddress: string,
98
101
  authResult: msal.AuthenticationResult
99
102
  ): Promise<EmailAccount> {
103
+ // MSAL Node doesn't expose refresh_token directly in AuthenticationResult
104
+ // but it is available if you use a cache plugin or direct refresh token request.
105
+ // For now, we'll store what we have.
100
106
  const { data, error } = await supabase
101
107
  .from('email_accounts')
102
108
  .upsert({
@@ -104,7 +110,7 @@ export class MicrosoftService {
104
110
  email_address: emailAddress,
105
111
  provider: 'outlook',
106
112
  access_token: authResult.accessToken,
107
- refresh_token: null, // MSAL handles token cache internally
113
+ refresh_token: (authResult as any).refreshToken || null,
108
114
  token_expires_at: authResult.expiresOn?.toISOString() || null,
109
115
  scopes: authResult.scopes,
110
116
  is_active: true,
@@ -117,6 +123,23 @@ export class MicrosoftService {
117
123
  return data;
118
124
  }
119
125
 
126
+ async getProviderCredentials(supabase: SupabaseClient, userId: string): Promise<{ clientId: string; clientSecret?: string; tenantId: string }> {
127
+ const { data: integration } = await supabase
128
+ .from('integrations')
129
+ .select('credentials')
130
+ .eq('user_id', userId)
131
+ .eq('provider', 'microsoft')
132
+ .single();
133
+
134
+ const creds = integration?.credentials as any;
135
+
136
+ return {
137
+ clientId: creds?.client_id || config.microsoft.clientId,
138
+ clientSecret: creds?.client_secret || config.microsoft.clientSecret,
139
+ tenantId: creds?.tenant_id || config.microsoft.tenantId || 'common'
140
+ };
141
+ }
142
+
120
143
  async refreshTokenIfNeeded(
121
144
  supabase: SupabaseClient,
122
145
  account: EmailAccount
@@ -133,14 +156,51 @@ export class MicrosoftService {
133
156
 
134
157
  logger.info('Refreshing Microsoft token', { accountId: account.id });
135
158
 
136
- // For Microsoft, we need the CCA with client secret for refresh
137
- if (!this.cca) {
138
- throw new Error('Microsoft client secret not configured for token refresh');
159
+ const refreshToken = account.refresh_token;
160
+
161
+ // If we have a refresh token stored, we can try to use the CCA
162
+ if (refreshToken) {
163
+ try {
164
+ const creds = await this.getProviderCredentials(supabase, account.user_id);
165
+ if (creds.clientSecret) {
166
+ const confidentialConfig: msal.Configuration = {
167
+ auth: {
168
+ clientId: creds.clientId,
169
+ authority: `https://login.microsoftonline.com/${creds.tenantId}`,
170
+ clientSecret: creds.clientSecret,
171
+ },
172
+ };
173
+ const cca = new msal.ConfidentialClientApplication(confidentialConfig);
174
+ const result = await cca.acquireTokenByRefreshToken({
175
+ refreshToken,
176
+ scopes: GRAPH_SCOPES,
177
+ });
178
+
179
+ if (result) {
180
+ const { data, error } = await supabase
181
+ .from('email_accounts')
182
+ .update({
183
+ access_token: result.accessToken,
184
+ refresh_token: (result as any).refreshToken || refreshToken, // Keep old if new not provided
185
+ token_expires_at: result.expiresOn?.toISOString() || null,
186
+ updated_at: new Date().toISOString(),
187
+ })
188
+ .eq('id', account.id)
189
+ .select()
190
+ .single();
191
+
192
+ if (error) throw error;
193
+ return data;
194
+ }
195
+ }
196
+ } catch (err) {
197
+ logger.warn('Confidential refresh failed, attempting public refresh...', { error: err });
198
+ }
139
199
  }
140
200
 
141
- // In production, you'd use refresh tokens stored in a token cache
142
- // This is a simplified implementation
143
- throw new Error('Microsoft token refresh requires re-authentication');
201
+ // Fallback or if no refresh token: we can't refresh automatically without a persistent cache
202
+ // Modern MSAL usually requires a TokenCache implementation for this
203
+ throw new Error('Outlook session expired. Please reconnect your account in Settings.');
144
204
  }
145
205
 
146
206
  async fetchMessages(
@@ -66,6 +66,8 @@ export class EmailProcessorService {
66
66
  let refreshedAccount = account;
67
67
  if (account.provider === 'gmail') {
68
68
  refreshedAccount = await this.gmailService.refreshTokenIfNeeded(this.supabase, account);
69
+ } else if (account.provider === 'outlook') {
70
+ refreshedAccount = await this.microsoftService.refreshTokenIfNeeded(this.supabase, account);
69
71
  }
70
72
 
71
73
  // Update status to syncing
@@ -95,10 +97,15 @@ export class EmailProcessorService {
95
97
  if (eventLogger) await eventLogger.info('Running', 'Starting sync process');
96
98
 
97
99
  // Process based on provider
98
- if (refreshedAccount.provider === 'gmail') {
99
- await this.processGmailAccount(refreshedAccount, rules || [], settings, result, eventLogger);
100
- } else if (refreshedAccount.provider === 'outlook') {
101
- await this.processOutlookAccount(refreshedAccount, rules || [], settings, result, eventLogger);
100
+ try {
101
+ if (refreshedAccount.provider === 'gmail') {
102
+ await this.processGmailAccount(refreshedAccount, rules || [], settings, result, eventLogger);
103
+ } else if (refreshedAccount.provider === 'outlook') {
104
+ await this.processOutlookAccount(refreshedAccount, rules || [], settings, result, eventLogger);
105
+ }
106
+ } catch (providerError) {
107
+ const providerName = refreshedAccount.provider === 'gmail' ? 'Gmail' : 'Outlook';
108
+ throw new Error(`${providerName} Sync Error: ${providerError instanceof Error ? providerError.message : String(providerError)}`);
102
109
  }
103
110
 
104
111
  // After processing new emails, run retention rules for this account
@@ -6,6 +6,9 @@ const GRAPH_SCOPES = [
6
6
  'https://graph.microsoft.com/Mail.Read',
7
7
  'https://graph.microsoft.com/Mail.ReadWrite',
8
8
  'https://graph.microsoft.com/User.Read',
9
+ 'offline_access',
10
+ 'openid',
11
+ 'profile',
9
12
  ];
10
13
  export class MicrosoftService {
11
14
  pca;
@@ -63,6 +66,9 @@ export class MicrosoftService {
63
66
  }
64
67
  }
65
68
  async saveAccount(supabase, userId, emailAddress, authResult) {
69
+ // MSAL Node doesn't expose refresh_token directly in AuthenticationResult
70
+ // but it is available if you use a cache plugin or direct refresh token request.
71
+ // For now, we'll store what we have.
66
72
  const { data, error } = await supabase
67
73
  .from('email_accounts')
68
74
  .upsert({
@@ -70,7 +76,7 @@ export class MicrosoftService {
70
76
  email_address: emailAddress,
71
77
  provider: 'outlook',
72
78
  access_token: authResult.accessToken,
73
- refresh_token: null, // MSAL handles token cache internally
79
+ refresh_token: authResult.refreshToken || null,
74
80
  token_expires_at: authResult.expiresOn?.toISOString() || null,
75
81
  scopes: authResult.scopes,
76
82
  is_active: true,
@@ -82,6 +88,20 @@ export class MicrosoftService {
82
88
  throw error;
83
89
  return data;
84
90
  }
91
+ async getProviderCredentials(supabase, userId) {
92
+ const { data: integration } = await supabase
93
+ .from('integrations')
94
+ .select('credentials')
95
+ .eq('user_id', userId)
96
+ .eq('provider', 'microsoft')
97
+ .single();
98
+ const creds = integration?.credentials;
99
+ return {
100
+ clientId: creds?.client_id || config.microsoft.clientId,
101
+ clientSecret: creds?.client_secret || config.microsoft.clientSecret,
102
+ tenantId: creds?.tenant_id || config.microsoft.tenantId || 'common'
103
+ };
104
+ }
85
105
  async refreshTokenIfNeeded(supabase, account) {
86
106
  if (!account.token_expires_at)
87
107
  return account;
@@ -92,13 +112,49 @@ export class MicrosoftService {
92
112
  return account;
93
113
  }
94
114
  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');
115
+ const refreshToken = account.refresh_token;
116
+ // If we have a refresh token stored, we can try to use the CCA
117
+ if (refreshToken) {
118
+ try {
119
+ const creds = await this.getProviderCredentials(supabase, account.user_id);
120
+ if (creds.clientSecret) {
121
+ const confidentialConfig = {
122
+ auth: {
123
+ clientId: creds.clientId,
124
+ authority: `https://login.microsoftonline.com/${creds.tenantId}`,
125
+ clientSecret: creds.clientSecret,
126
+ },
127
+ };
128
+ const cca = new msal.ConfidentialClientApplication(confidentialConfig);
129
+ const result = await cca.acquireTokenByRefreshToken({
130
+ refreshToken,
131
+ scopes: GRAPH_SCOPES,
132
+ });
133
+ if (result) {
134
+ const { data, error } = await supabase
135
+ .from('email_accounts')
136
+ .update({
137
+ access_token: result.accessToken,
138
+ refresh_token: result.refreshToken || refreshToken, // Keep old if new not provided
139
+ token_expires_at: result.expiresOn?.toISOString() || null,
140
+ updated_at: new Date().toISOString(),
141
+ })
142
+ .eq('id', account.id)
143
+ .select()
144
+ .single();
145
+ if (error)
146
+ throw error;
147
+ return data;
148
+ }
149
+ }
150
+ }
151
+ catch (err) {
152
+ logger.warn('Confidential refresh failed, attempting public refresh...', { error: err });
153
+ }
98
154
  }
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');
155
+ // Fallback or if no refresh token: we can't refresh automatically without a persistent cache
156
+ // Modern MSAL usually requires a TokenCache implementation for this
157
+ throw new Error('Outlook session expired. Please reconnect your account in Settings.');
102
158
  }
103
159
  async fetchMessages(account, options = {}) {
104
160
  const accessToken = account.access_token || '';
@@ -49,6 +49,9 @@ export class EmailProcessorService {
49
49
  if (account.provider === 'gmail') {
50
50
  refreshedAccount = await this.gmailService.refreshTokenIfNeeded(this.supabase, account);
51
51
  }
52
+ else if (account.provider === 'outlook') {
53
+ refreshedAccount = await this.microsoftService.refreshTokenIfNeeded(this.supabase, account);
54
+ }
52
55
  // Update status to syncing
53
56
  await this.supabase
54
57
  .from('email_accounts')
@@ -73,11 +76,17 @@ export class EmailProcessorService {
73
76
  if (eventLogger)
74
77
  await eventLogger.info('Running', 'Starting sync process');
75
78
  // Process based on provider
76
- if (refreshedAccount.provider === 'gmail') {
77
- await this.processGmailAccount(refreshedAccount, rules || [], settings, result, eventLogger);
79
+ try {
80
+ if (refreshedAccount.provider === 'gmail') {
81
+ await this.processGmailAccount(refreshedAccount, rules || [], settings, result, eventLogger);
82
+ }
83
+ else if (refreshedAccount.provider === 'outlook') {
84
+ await this.processOutlookAccount(refreshedAccount, rules || [], settings, result, eventLogger);
85
+ }
78
86
  }
79
- else if (refreshedAccount.provider === 'outlook') {
80
- await this.processOutlookAccount(refreshedAccount, rules || [], settings, result, eventLogger);
87
+ catch (providerError) {
88
+ const providerName = refreshedAccount.provider === 'gmail' ? 'Gmail' : 'Outlook';
89
+ throw new Error(`${providerName} Sync Error: ${providerError instanceof Error ? providerError.message : String(providerError)}`);
81
90
  }
82
91
  // After processing new emails, run retention rules for this account
83
92
  await this.runRetentionRules(refreshedAccount, rules || [], settings, result, eventLogger);