try to commit

This commit is contained in:
2026-07-14 12:34:40 +08:00
parent 17755fce7e
commit a7a72096a5
6 changed files with 167 additions and 61 deletions
+61 -35
View File
@@ -1,35 +1,66 @@
/***********************************************************************************************************************************************************************
* File Name: email.service.js
* Type of Program: Service
* Description: Nodemailer-based email 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 nodemailer = require('nodemailer');
const axios = require('axios');
const { OAuth2Client } = require('google-auth-library');
const { emailTemplates } = require('../data/email_body.data');
const port = Number(process.env.SMTP_PORT);
const GMAIL_SEND_URL = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send';
const GMAIL_PROFILE_URL = 'https://gmail.googleapis.com/gmail/v1/users/me/profile';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port,
secure: port === 465,
requireTLS: port === 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
tls: {
rejectUnauthorized: false,
},
});
// 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 {
@@ -40,29 +71,24 @@ const sendEmail = async ({ to, type, data = {} }) => {
}
const { subject, html } = templateFn(data);
const accessToken = await getAccessToken();
return await new Promise((resolve, reject) => {
transporter.sendMail(
{
from: {
name: "STARR System",
address: process.env.EMAIL_FROM,
},
to,
subject,
html,
},
(err, info) => {
if (err) return reject(err);
resolve(info);
}
);
});
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.message);
throw new Error(err.response?.data?.error?.message || err.message);
}
};
const ping = () => transporter.verify();
// Confirms the refresh token is valid and the Gmail API is reachable.
const ping = async () => {
const accessToken = await getAccessToken();
await axios.get(GMAIL_PROFILE_URL, { headers: { Authorization: `Bearer ${accessToken}` } });
};
module.exports = { sendEmail, ping };