mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
try to commit
This commit is contained in:
+61
-35
@@ -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 };
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* ✓ database — PostgreSQL via Sequelize (CRITICAL)
|
||||
* ✓ cache — Redis PING; skipped in memory mode (optional)
|
||||
* ✓ storage — S3/Garage HeadBucket (optional)
|
||||
* ✓ smtp — Nodemailer connection verify (optional)
|
||||
* ✓ email — Gmail API profile fetch (OAuth2) (optional)
|
||||
*
|
||||
* Checks intentionally excluded:
|
||||
* ✗ chibisafe — third-party CDN; not owned infrastructure
|
||||
@@ -35,8 +35,8 @@ const os = require('os');
|
||||
|
||||
const sequelize = require('../config/db.config');
|
||||
const redis = require('../config/redis.config');
|
||||
const { ping: pingS3 } = require('./s3.service');
|
||||
const { ping: pingSmtp } = require('./email.service');
|
||||
const { ping: pingS3 } = require('./s3.service');
|
||||
const { ping: pingEmail } = require('./email.service');
|
||||
|
||||
const PKG = require('../package.json');
|
||||
const TIMEOUT_MS = 3000;
|
||||
@@ -85,12 +85,12 @@ const CHECKS = [
|
||||
run: pingS3,
|
||||
},
|
||||
{
|
||||
name: 'smtp',
|
||||
name: 'email',
|
||||
critical: false,
|
||||
enabled: !!process.env.SMTP_HOST,
|
||||
skipNote: 'SMTP_HOST is not configured',
|
||||
meta: { host: process.env.SMTP_HOST, port: Number(process.env.SMTP_PORT) || 587 },
|
||||
run: pingSmtp,
|
||||
enabled: !!(process.env.GOOGLE_CLIENT_ID && process.env.GMAIL_REFRESH_TOKEN),
|
||||
skipNote: 'GMAIL_REFRESH_TOKEN is not configured',
|
||||
meta: { provider: 'gmail-api', account: process.env.EMAIL_FROM },
|
||||
run: pingEmail,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user