@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.
- package/api/src/services/microsoft.ts +67 -7
- package/api/src/services/processor.ts +11 -4
- package/dist/api/src/services/microsoft.js +63 -7
- package/dist/api/src/services/processor.js +13 -4
- package/dist/assets/{index-1LhqbPPU.js → index-Bc3ujqpb.js} +3 -3
- package/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -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:
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
//
|
|
142
|
-
//
|
|
143
|
-
throw new Error('
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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,
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
//
|
|
100
|
-
//
|
|
101
|
-
throw new Error('
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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);
|