UNPKG

@checkfirst/nestjs-outlook

Version:

An opinionated NestJS module for Microsoft Outlook integration that provides easy access to Microsoft Graph API for emails, calendars, and more.

247 lines 14 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var EmailService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.EmailService = void 0; const common_1 = require("@nestjs/common"); const event_emitter_1 = require("@nestjs/event-emitter"); const microsoft_graph_client_1 = require("@microsoft/microsoft-graph-client"); const axios_1 = require("axios"); const microsoft_auth_service_1 = require("../auth/microsoft-auth.service"); const constants_1 = require("../../constants"); const outlook_webhook_subscription_repository_1 = require("../../repositories/outlook-webhook-subscription.repository"); const event_types_enum_1 = require("../../enums/event-types.enum"); const microsoft_user_entity_1 = require("../../entities/microsoft-user.entity"); const typeorm_1 = require("@nestjs/typeorm"); const typeorm_2 = require("typeorm"); const user_id_converter_service_1 = require("../shared/user-id-converter.service"); let EmailService = EmailService_1 = class EmailService { constructor(microsoftAuthService, webhookSubscriptionRepository, eventEmitter, microsoftConfig, microsoftUserRepository, userIdConverter) { this.microsoftAuthService = microsoftAuthService; this.webhookSubscriptionRepository = webhookSubscriptionRepository; this.eventEmitter = eventEmitter; this.microsoftConfig = microsoftConfig; this.microsoftUserRepository = microsoftUserRepository; this.userIdConverter = userIdConverter; this.logger = new common_1.Logger(EmailService_1.name); } async sendEmail(message, externalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessToken({ externalUserId }); const client = microsoft_graph_client_1.Client.init({ authProvider: (done) => { done(null, accessToken); }, }); const sentMessage = await client .api('/me/sendMail') .post({ message }); return { message: sentMessage, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error(`Failed to send email: ${errorMessage}`); throw new Error(`Failed to send email: ${errorMessage}`); } } async createWebhookSubscription(externalUserId) { try { const internalUserId = await this.userIdConverter.externalToInternal(externalUserId); const accessToken = await this.microsoftAuthService.getUserAccessToken({ internalUserId }); const expirationDateTime = new Date(); expirationDateTime.setHours(expirationDateTime.getHours() + 72); const appUrl = this.microsoftConfig.backendBaseUrl || 'http://localhost:3000'; const basePath = this.microsoftConfig.basePath; const basePathUrl = basePath ? `${appUrl}/${basePath}` : appUrl; const notificationUrl = `${basePathUrl}/email/webhook`; const subscriptionData = { changeType: 'created,updated,deleted', notificationUrl, lifecycleNotificationUrl: notificationUrl, resource: '/me/messages', expirationDateTime: expirationDateTime.toISOString(), clientState: `user_${internalUserId}_${Math.random().toString(36).substring(2, 15)}`, }; this.logger.debug(`Creating email webhook subscription with notificationUrl: ${notificationUrl}`); this.logger.debug(`Subscription data: ${JSON.stringify(subscriptionData)}`); const response = await axios_1.default.post('https://graph.microsoft.com/v1.0/subscriptions', subscriptionData, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); this.logger.log(`Created email webhook subscription ${response.data.id || 'unknown'} for user ${internalUserId}`); await this.webhookSubscriptionRepository.saveSubscription({ subscriptionId: response.data.id, userId: internalUserId, resource: response.data.resource, changeType: response.data.changeType, clientState: response.data.clientState || '', notificationUrl: response.data.notificationUrl, expirationDateTime: response.data.expirationDateTime ? new Date(response.data.expirationDateTime) : new Date(), }); this.logger.debug(`Stored subscription`); return response.data; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error(`Failed to create email webhook subscription: ${errorMessage}`); throw new Error(`Failed to create email webhook subscription: ${errorMessage}`); } } async deleteWebhookSubscription(subscriptionId, userId) { var _a; const correlationId = `delete-email-sub-${subscriptionId}-${Date.now()}`; try { this.logger.log(`[${correlationId}] Deleting email subscription ${subscriptionId} for user ${userId}`); const internalUserId = await this.userIdConverter.toInternalUserId(userId); const accessToken = await this.microsoftAuthService.getUserAccessToken({ internalUserId, includeInactive: true }); this.logger.debug(`[${correlationId}] Access token obtained`); this.logger.debug(`[${correlationId}] Calling Microsoft Graph API to delete email subscription`); await axios_1.default.delete(`https://graph.microsoft.com/v1.0/subscriptions/${subscriptionId}`, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); this.logger.log(`[${correlationId}] Successfully deleted email subscription at Microsoft`); await this.webhookSubscriptionRepository.deactivateSubscription(subscriptionId); this.logger.debug(`[${correlationId}] Deactivated email subscription in local database`); const otherActiveSubscriptions = await this.webhookSubscriptionRepository.count({ where: { userId: internalUserId, isActive: true, subscriptionId: (0, typeorm_2.Not)(subscriptionId) } }); if (otherActiveSubscriptions === 0) { const updateCriteria = typeof userId === 'string' ? { externalUserId: userId } : { id: userId }; await this.microsoftUserRepository.update(updateCriteria, { isActive: false }); this.logger.debug(`[${correlationId}] Marked Microsoft user as inactive - no other subscriptions remain`); } else { this.logger.debug(`[${correlationId}] User still has ${otherActiveSubscriptions} other active subscription(s), keeping user active`); } this.logger.log(`[${correlationId}] Successfully deleted email subscription ${subscriptionId}`); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404) { this.logger.log(`[${correlationId}] Email subscription ${subscriptionId} not found at Microsoft, ` + `cleaning up local database`); await this.webhookSubscriptionRepository.deactivateSubscription(subscriptionId); this.logger.log(`[${correlationId}] Successfully cleaned up orphaned email subscription ${subscriptionId}`); return true; } this.logger.error(`[${correlationId}] Failed to delete email subscription ${subscriptionId}: ${errorMessage}`); throw new Error(`Failed to delete email webhook subscription: ${errorMessage}`); } } async handleEmailWebhook(notificationItem) { try { const { subscriptionId, clientState, resource, changeType } = notificationItem; this.logger.debug(`Received email webhook notification for subscription: ${subscriptionId || 'unknown'}`); this.logger.debug(`Resource: ${resource || 'unknown'}, ChangeType: ${String(changeType || 'unknown')}`); const subscription = await this.webhookSubscriptionRepository.findBySubscriptionId(subscriptionId || ''); if (!subscription) { this.logger.warn(`Unknown subscription ID: ${subscriptionId || 'unknown'}`); return { success: false, message: 'Unknown subscription' }; } if (subscription.clientState && clientState !== subscription.clientState) { this.logger.warn('Client state mismatch'); return { success: false, message: 'Client state mismatch' }; } const internalUserId = subscription.userId; if (!internalUserId) { this.logger.warn('Could not determine user ID from client state'); return { success: false, message: 'Invalid client state format' }; } let eventType; switch (changeType) { case 'created': eventType = event_types_enum_1.OutlookEventTypes.EMAIL_RECEIVED; break; case 'updated': eventType = event_types_enum_1.OutlookEventTypes.EMAIL_UPDATED; break; case 'deleted': eventType = event_types_enum_1.OutlookEventTypes.EMAIL_DELETED; break; default: eventType = null; this.logger.warn(`Unknown change type received: ${String(changeType)}`); return { success: false, message: `Unsupported change type: ${String(changeType)}` }; } let emailData = {}; if (changeType === 'created' && resource) { try { const messageId = resource.split('/').pop(); if (messageId) { const accessToken = await this.microsoftAuthService.getUserAccessToken({ internalUserId }); const client = microsoft_graph_client_1.Client.init({ authProvider: (done) => { done(null, accessToken); }, }); emailData = await client .api(`/me/messages/${messageId}`) .select('id,subject,receivedDateTime,from,toRecipients,ccRecipients,body') .get(); this.logger.log(`Retrieved email details for message ID: ${messageId}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error(`Failed to retrieve email details: ${errorMessage}`); } } const resourceData = { id: '', userId: internalUserId, subscriptionId, resource, changeType, data: emailData }; if (eventType) { this.eventEmitter.emit(eventType, resourceData); this.logger.log(`Processed email webhook notification: ${eventType}`); } return { success: true, message: 'Notification processed' }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error(`Error processing email webhook notification: ${errorMessage}`); return { success: false, message: errorMessage }; } } }; exports.EmailService = EmailService; exports.EmailService = EmailService = EmailService_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, common_1.Inject)((0, common_1.forwardRef)(() => microsoft_auth_service_1.MicrosoftAuthService))), __param(3, (0, common_1.Inject)(constants_1.MICROSOFT_CONFIG)), __param(4, (0, typeorm_1.InjectRepository)(microsoft_user_entity_1.MicrosoftUser)), __metadata("design:paramtypes", [microsoft_auth_service_1.MicrosoftAuthService, outlook_webhook_subscription_repository_1.OutlookWebhookSubscriptionRepository, event_emitter_1.EventEmitter2, Object, typeorm_2.Repository, user_id_converter_service_1.UserIdConverterService]) ], EmailService); //# sourceMappingURL=email.service.js.map