EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
emailAutomation.js
Go to the documentation of this file.
1/**
2 * @file emailAutomation.js
3 * @module routes/emailAutomation
4 * @description Email-to-ticket automation endpoints with Microsoft Graph OAuth integration.
5 * Handles inbound email processing, tenant email settings, OAuth flow for Office 365,
6 * and outbound email sending via Graph API, SMTP, or Mailgun.
7 * @requires express
8 * @requires pushEmailToRedis
9 * @requires uuid
10 * @requires @azure/msal-node
11 * @requires services/db
12 * @requires services/email
13 * @author RMM-PSA Development Team
14 * @copyright 2026 RMM-PSA Platform
15 * @license Proprietary
16 */
17
18/**
19 * @apiDefine EmailAutomation Email Automation
20 * Email-to-ticket automation and Microsoft Graph integration
21 */
22
23const express = require('express');
24const router = express.Router();
25const { pushEmail } = require('../pushEmailToRedis');
26const { v4: uuidv4 } = require('uuid');
27const msal = require('@azure/msal-node');
28
29/**
30 * @api {post} /api/email/inbound Process inbound email
31 * @apiName ProcessInboundEmail
32 * @apiGroup EmailAutomation
33 * @apiDescription Receive inbound emails and queue for AI parsing to create tickets.
34 * Generates unique email ID, pushes to Redis queue for async processing by AI worker.
35 * No authentication required (called by email webhook/IMAP forwarder).
36 * @apiParam {string} from Sender email address
37 * @apiParam {string} [to] Recipient email address
38 * @apiParam {string} subject Email subject
39 * @apiParam {string} [text] Plain text email body
40 * @apiParam {string} [html] HTML email body (used if text not provided)
41 * @apiSuccess {string} status="queued" Email queued for processing
42 * @apiSuccess {string} emailId Generated UUID for email tracking
43 * @apiError (400) {String} error="Missing required fields" From, subject, or body missing
44 * @apiExample {curl} Example:
45 * curl -X POST http://localhost:3000/api/email/inbound \\
46 * -H "Content-Type: application/json" \\
47 * -d '{
48 * "from": "customer@example.com",
49 * "to": "support@msp.com",
50 * "subject": "Printer not working",
51 * "text": "My printer has stopped working since this morning."
52 * }'
53 * @apiSuccessExample {json} Success-Response:
54 * HTTP/1.1 200 OK
55 * {
56 * "status": "queued",
57 * "emailId": "550e8400-e29b-41d4-a716-446655440000"
58 * }
59 */
60router.post('/inbound', async (req, res) => {
61 const { from, to, subject, text, html } = req.body;
62 if (!from || !subject || !(text || html)) {
63 return res.status(400).json({ error: 'Missing required fields' });
64 }
65 // Generate a unique email ID
66 const emailId = uuidv4();
67 // Push to Redis for AI parsing
68 pushEmail({ id: emailId, from, to, subject, text: text || html });
69 res.json({ status: 'queued', emailId });
70});
71
72/**
73 * @api {get} /api/email/settings/:tenant_id Get email settings
74 * @apiName GetEmailSettings
75 * @apiGroup EmailAutomation
76 * @apiDescription Retrieve tenant email automation settings from database.
77 * Returns email configuration (provider, credentials, OAuth tokens).
78 * @apiParam {number} tenant_id Tenant ID
79 * @apiSuccess {string} [provider] Email provider ("smtp", "mailgun", "office365")
80 * @apiSuccess {string} [username] Email username/address
81 * @apiSuccess {object} [graph] Microsoft Graph OAuth tokens (if using Office 365)
82 * @apiSuccess {object} [smtp] SMTP server configuration
83 * @apiSuccess {object} [mailgun] Mailgun API configuration
84 * @apiError (404) {String} error="Tenant not found" Tenant ID not in database
85 * @apiError (500) {String} error="Server error" Database query failed
86 * @apiExample {curl} Example:
87 * curl -X GET http://localhost:3000/api/email/settings/2
88 * @apiSuccessExample {json} Success-Response:
89 * HTTP/1.1 200 OK
90 * {
91 * "provider": "office365",
92 * "username": "support@acme.com",
93 * "graph": {
94 * "accessToken": "eyJ0eXAiOi...",
95 * "refreshToken": "M.R3_BAY..."
96 * }
97 * }
98 */
99router.get('/settings/:tenant_id', async (req, res) => {
100 const { tenant_id } = req.params;
101 try {
102 const pool = require('../services/db');
103 const result = await pool.query('SELECT email_automation FROM tenants WHERE tenant_id = $1', [tenant_id]);
104 if (!result.rows.length) return res.status(404).json({ error: 'Tenant not found' });
105 res.json(result.rows[0].email_automation || {});
106 } catch (err) {
107 res.status(500).json({ error: 'Server error' });
108 }
109});
110
111/**
112 * @api {post} /api/email/settings/:tenant_id Save email settings
113 * @apiName SaveEmailSettings
114 * @apiGroup EmailAutomation
115 * @apiDescription Update tenant email automation settings in database.
116 * Accepts full settings object (provider, credentials, tokens).
117 * @apiParam {number} tenant_id Tenant ID
118 * @apiParam {string} [provider] Email provider ("smtp", "mailgun", "office365")
119 * @apiParam {string} [username] Email username/address
120 * @apiParam {object} [smtp] SMTP server configuration
121 * @apiParam {string} [smtp.host] SMTP server hostname
122 * @apiParam {number} [smtp.port] SMTP port
123 * @apiParam {string} [smtp.password] SMTP password
124 * @apiParam {object} [mailgun] Mailgun API configuration
125 * @apiParam {string} [mailgun.apiKey] Mailgun API key
126 * @apiParam {string} [mailgun.domain] Mailgun domain
127 * @apiSuccess {boolean} success=true Settings saved successfully
128 * @apiError (500) {String} error="Server error" Database update failed
129 * @apiExample {curl} Example:
130 * curl -X POST http://localhost:3000/api/email/settings/2 \\
131 * -H "Content-Type: application/json" \\
132 * -d '{
133 * "provider": "smtp",
134 * "username": "support@acme.com",
135 * "smtp": {
136 * "host": "mail.acme.com",
137 * "port": 587,
138 * "password": "secret123"
139 * }
140 * }'
141 * @apiSuccessExample {json} Success-Response:
142 * HTTP/1.1 200 OK
143 * {
144 * "success": true
145 * }
146 */
147router.post('/settings/:tenant_id', async (req, res) => {
148 const { tenant_id } = req.params;
149 const settings = req.body;
150 try {
151 const pool = require('../services/db');
152 await pool.query('UPDATE tenants SET email_automation = $1 WHERE tenant_id = $2', [JSON.stringify(settings), tenant_id]);
153 res.json({ success: true });
154 } catch (err) {
155 res.status(500).json({ error: 'Server error' });
156 }
157});
158
159/**
160 * @api {post} /api/email/oauth/:tenant_id Trigger OAuth flow (stub)
161 * @apiName TriggerEmailOAuth
162 * @apiGroup EmailAutomation
163 * @apiDescription Stub endpoint for triggering Microsoft Graph OAuth flow.
164 * Currently not implemented - returns placeholder response.
165 * Use GET /oauth/:tenant_id/start for actual OAuth initiation.
166 * @apiParam {number} tenant_id Tenant ID
167 * @apiSuccess {string} status="not_implemented"
168 * @apiSuccess {string} message Placeholder message
169 * @apiExample {curl} Example:
170 * curl -X POST http://localhost:3000/api/email/oauth/2
171 * @apiSuccessExample {json} Success-Response:
172 * HTTP/1.1 200 OK
173 * {
174 * "status": "not_implemented",
175 * "message": "OAuth flow not yet implemented."
176 * }
177 */
178router.post('/oauth/:tenant_id', async (req, res) => {
179 // TODO: Implement Microsoft Graph OAuth flow and store tokens in email_automation
180 res.json({ status: 'not_implemented', message: 'OAuth flow not yet implemented.' });
181});
182
183/**
184 * @api {get} /api/email/oauth/:tenant_id/start Start OAuth flow
185 * @apiName StartEmailOAuth
186 * @apiGroup EmailAutomation
187 * @apiDescription Initiate Microsoft Graph OAuth2 flow for Office 365 email integration.
188 * Generates authorization URL using MSAL. User redirects to Microsoft login.
189 * Requires msalConfig with clientId, authority configured.
190 * @apiParam {number} tenant_id Tenant ID (used in OAuth state parameter)
191 * @apiSuccess {string} url Microsoft authorization URL for user redirect
192 * @apiError (500) {String} error="Failed to generate auth URL" MSAL configuration error
193 * @apiExample {curl} Example:
194 * curl -X GET http://localhost:3000/api/email/oauth/2/start
195 * @apiSuccessExample {json} Success-Response:
196 * HTTP/1.1 200 OK
197 * {
198 * "url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=..."
199 * }
200 */
201router.get('/oauth/:tenant_id/start', async (req, res) => {
202 const { tenant_id } = req.params;
203 const msalClient = new msal.ConfidentialClientApplication(msalConfig);
204 const authCodeUrlParameters = {
205 scopes: ['offline_access', 'Mail.Read', 'Mail.Send'],
206 redirectUri
207 };
208 try {
209 const url = await msalClient.getAuthCodeUrl(authCodeUrlParameters);
210 res.json({ url });
211 } catch (err) {
212 res.status(500).json({ error: 'Failed to generate auth URL' });
213 }
214});
215
216/**
217 * @api {get} /api/email/oauth/callback OAuth callback
218 * @apiName EmailOAuthCallback
219 * @apiGroup EmailAutomation
220 * @apiDescription Handle Microsoft Graph OAuth callback after user authorization.
221 * Exchanges authorization code for access/refresh tokens using MSAL.
222 * Stores tokens in tenant email_automation settings.
223 * @apiParam {string} code Authorization code (query parameter from Microsoft)
224 * @apiParam {string} state Tenant ID passed as state parameter (query parameter)
225 * @apiSuccess {boolean} success=true Tokens acquired and saved successfully
226 * @apiError (400) {String} error="Missing code or tenant_id" Required parameters not provided
227 * @apiError (500) {String} error="OAuth callback failed" Token exchange or database save failed
228 * @apiExample {curl} Example:
229 * curl -X GET "http://localhost:3000/api/email/oauth/callback?code=M.R3_BAY...&state=2"
230 * @apiSuccessExample {json} Success-Response:
231 * HTTP/1.1 200 OK
232 * {
233 * "success": true
234 * }
235 */
236router.get('/oauth/callback', async (req, res) => {
237 const code = req.query.code;
238 const tenant_id = req.query.state; // Pass tenant_id in state param for security
239 if (!code || !tenant_id) return res.status(400).json({ error: 'Missing code or tenant_id' });
240 const msalClient = new msal.ConfidentialClientApplication(msalConfig);
241 try {
242 const tokenResponse = await msalClient.acquireTokenByCode({
243 code,
244 scopes: ['offline_access', 'Mail.Read', 'Mail.Send'],
245 redirectUri
246 });
247 // Save tokens to tenant email_automation config
248 await pool.query(
249 "UPDATE tenants SET email_automation = jsonb_set(coalesce(email_automation, '{}'), '{graph}', $1::jsonb) WHERE tenant_id = $2",
250 [JSON.stringify(tokenResponse), tenant_id]
251 );
252 res.json({ success: true });
253 } catch (err) {
254 res.status(500).json({ error: 'OAuth callback failed', details: err.message });
255 }
256});
257
258/**
259 * @api {post} /api/email/test/:tenant_id Test email connection
260 * @apiName TestEmailConnection
261 * @apiGroup EmailAutomation
262 * @apiDescription Test tenant email connection settings (stub implementation).
263 * Verifies email configuration exists. TODO: Implement actual connection test (IMAP/Graph API).
264 * @apiParam {number} tenant_id Tenant ID
265 * @apiSuccess {boolean} success=true Settings found (not actual connection test)
266 * @apiSuccess {string} message Stub message
267 * @apiError (404) {String} error="Tenant not found" Tenant ID not in database
268 * @apiError (400) {String} error="No email settings found" Email automation not configured
269 * @apiError (500) {String} error="Server error" Database query failed
270 * @apiExample {curl} Example:
271 * curl -X POST http://localhost:3000/api/email/test/2
272 * @apiSuccessExample {json} Success-Response:
273 * HTTP/1.1 200 OK
274 * {
275 * "success": true,
276 * "message": "Connection test stub (implement real test)"
277 * }
278 */
279router.post('/test/:tenant_id', async (req, res) => {
280 const { tenant_id } = req.params;
281 try {
282 const pool = require('../services/db');
283 const result = await pool.query('SELECT email_automation FROM tenants WHERE tenant_id = $1', [tenant_id]);
284 if (!result.rows.length) return res.status(404).json({ error: 'Tenant not found' });
285 const settings = result.rows[0].email_automation;
286 // TODO: Actually test connection (IMAP/Graph API)
287 if (!settings || !settings.username) return res.status(400).json({ error: 'No email settings found' });
288 res.json({ success: true, message: 'Connection test stub (implement real test)' });
289 } catch (err) {
290 res.status(500).json({ error: 'Server error' });
291 }
292});
293
294/**
295 * @api {post} /api/email/send/:tenant_id Send email
296 * @apiName SendEmail
297 * @apiGroup EmailAutomation
298 * @apiDescription Send email via Microsoft Graph, SMTP, or Mailgun.
299 * Uses tenant email automation settings to determine provider and credentials.
300 * Supports Office 365 Graph API, generic SMTP, or Mailgun.
301 * @apiParam {number} tenant_id Tenant ID
302 * @apiParam {string} to Recipient email address
303 * @apiParam {string} subject Email subject
304 * @apiParam {string} [body] Email body (HTML or text, deprecated - use html/text)
305 * @apiParam {string} [html] HTML email body
306 * @apiParam {string} [text] Plain text email body
307 * @apiParam {string} [provider] Override provider ("office365", "smtp", "mailgun")
308 * @apiSuccess {boolean} success=true Email sent successfully
309 * @apiSuccess {Object} [result] Send result details (SMTP/Mailgun only)
310 * @apiError (400) {String} error="Missing to, subject, or body/html/text" Required parameters missing
311 * @apiError (404) {String} error="Tenant not found" Tenant ID not in database
312 * @apiError (400) {String} error="No Graph access token found" Office 365 tokens not configured
313 * @apiError (500) {String} error="Failed to send email" Send operation failed
314 * @apiExample {curl} Example (Office 365):
315 * curl -X POST http://localhost:3000/api/email/send/2 \\
316 * -H "Content-Type: application/json" \\
317 * -d '{
318 * "to": "customer@example.com",
319 * "subject": "Ticket Update",
320 * "html": "<p>Your ticket has been resolved.</p>",
321 * "provider": "office365"
322 * }'
323 * @apiSuccessExample {json} Success-Response:
324 * HTTP/1.1 200 OK
325 * {
326 * "success": true
327 * }
328 */
329router.post('/send/:tenant_id', async (req, res) => {
330 const { tenant_id } = req.params;
331 const { to, subject, body, html, text, provider } = req.body;
332 if (!to || !subject || !(body || html || text)) return res.status(400).json({ error: 'Missing to, subject, or body/html/text' });
333 try {
334 const pool = require('../services/db');
335 const { sendEmail } = require('../services/email');
336 // Get tenant email settings
337 const result = await pool.query('SELECT email_automation FROM tenants WHERE tenant_id = $1', [tenant_id]);
338 if (!result.rows.length) return res.status(404).json({ error: 'Tenant not found' });
339 const settings = result.rows[0].email_automation || {};
340 // Determine provider: 'mailgun', 'smtp', or 'office365' (graph)
341 let emailProvider = provider || settings.provider || 'smtp';
342 // If office365/graph, use old logic
343 if (emailProvider === 'office365' || emailProvider === 'graph') {
344 const graph = settings.graph;
345 if (!graph || !graph.accessToken) return res.status(400).json({ error: 'No Graph access token found' });
346 const { Client } = require('@microsoft/microsoft-graph-client');
347 require('isomorphic-fetch');
348 // TODO: Refresh token if expired
349 const client = Client.init({
350 authProvider: (done) => { done(null, graph.accessToken); }
351 });
352 await client.api('/me/sendMail').post({
353 message: {
354 subject,
355 body: { contentType: 'HTML', content: body || html },
356 toRecipients: [{ emailAddress: { address: to } }]
357 },
358 saveToSentItems: true
359 });
360 return res.json({ success: true });
361 }
362 // Otherwise, use sendEmail (Mailgun/SMTP)
363 const emailResult = await sendEmail({
364 to,
365 subject,
366 html: html || body,
367 text,
368 provider: emailProvider,
369 settings
370 });
371 res.json({ success: true, result: emailResult });
372 } catch (err) {
373 res.status(500).json({ error: 'Failed to send email', details: err.message });
374 }
375});
376
377// This route can be called by your email parser (IMAP/SMTP webhook)
378// The AI worker will process and create a ticket if needed
379module.exports = router;