/*********************************************************************************************************************************************************************** * File Name: email.service.js * Type of Program: Service * Description: Gmail API-based email service (HTTPS, gmail.googleapis.com). * Render blocks outbound raw SMTP (ports 25/465/587), so nodemailer's * SMTP transport to smtp.gmail.com can never connect from this host — * every send failed with nodemailer's own "Connection timeout" after * its 2-minute connectionTimeout expired. Sending over the Gmail REST * API instead rides on HTTPS/443, which isn't blocked. * Auth is OAuth2 with a long-lived refresh token for services.philpro@gmail.com * (see scripts/get_gmail_refresh_token.js to mint one), reusing the same * GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET already registered for Google login. * Subject + body per type are hardcoded in data/email_body.data.js — * there is no admin UI or DB table. To add or change an email, * edit that file directly and redeploy. * Author: rgrgogu, Kenneth Obsequio (@lash0000) * Date Created: Oct. 6, 2025 * Date Modified: Jul. 14, 2026 — Replaced nodemailer/SMTP with Gmail API over HTTPS; * SMTP was silently black-holed by Render's network. (Kenneth Obsequio) *********************************************************************************************************************************************************************** * HOW TO USE: * const emailService = require('../services/email.service'); * await emailService.sendEmail({ to: user.email, type: 'OTP', data: { otp } }); ***********************************************************************************************************************************************************************/ const axios = require('axios'); const { OAuth2Client } = require('google-auth-library'); const { emailTemplates } = require('../data/email_body.data'); const GMAIL_SEND_URL = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'; // Lazily initialised so the module can be required before env is loaded. let _oauth2Client; const getOAuth2Client = () => { if (!_oauth2Client) { _oauth2Client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET); _oauth2Client.setCredentials({ refresh_token: process.env.GMAIL_REFRESH_TOKEN }); } return _oauth2Client; }; const getAccessToken = async () => { const { token } = await getOAuth2Client().getAccessToken(); if (!token) throw new Error('Failed to obtain Gmail access token'); return token; }; // Encodes a MIME header value so non-ASCII subjects survive transit (RFC 2047). const encodeHeader = (value) => `=?UTF-8?B?${Buffer.from(value, 'utf-8').toString('base64')}?=`; const buildRawMessage = ({ to, subject, html }) => { const message = [ `From: "STARR System" <${process.env.EMAIL_FROM}>`, `To: ${to}`, `Subject: ${encodeHeader(subject)}`, 'MIME-Version: 1.0', 'Content-Type: text/html; charset=UTF-8', '', html, ].join('\r\n'); return Buffer.from(message).toString('base64url'); }; const sendEmail = async ({ to, type, data = {} }) => { try { const templateFn = emailTemplates[type]; if (!templateFn) { throw new Error(`Email template "${type}" not found`); } const { subject, html } = templateFn(data); const accessToken = await getAccessToken(); const { data: result } = await axios.post( GMAIL_SEND_URL, { raw: buildRawMessage({ to, subject, html }) }, { headers: { Authorization: `Bearer ${accessToken}` } }, ); return result; } catch (err) { throw new Error(err.response?.data?.error?.message || err.message); } }; // Confirms the refresh token is valid and Gmail's OAuth endpoint is reachable. // Deliberately doesn't call a Gmail read endpoint (e.g. users.getProfile) — the // refresh token is scoped to gmail.send only, which doesn't grant read access. const ping = () => getAccessToken(); module.exports = { sendEmail, ping };