Skip to content

Commit 878da0d

Browse files
committedNov 16, 2021
Compile ts to js
1 parent 95fe51c commit 878da0d

11 files changed

+265
-19
lines changed
 

‎.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"files": ["src/**/*.ts"],
23
"env": {
34
"browser": true,
45
"es2021": true

‎.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
config/
22
credentials/
33
node_modules/
4-
token/
4+
token/
5+
6+
whitelist.json

‎README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ Send emails using Gmail API
55
1. Place your credential file in `credentials` folder with name `credentials.{{account}}.json`.
66
2. Create `.env` file in `config` folder.
77
- You can set value of HTTP_PORT and HTTPS_PORT.
8-
3. (Optional) Edit the `whitelist.json` file to config CORS.
9-
4. Run `npm run i` or `npm run ci`.
10-
5. Run `npm run init`.
8+
3. Run `npm run init`.
119
- Enter the account name to create an token file.
1210
- Token file will be created in `token` folder.
11+
4. (Optional) Edit the `whitelist.json` file to config CORS.
12+
5. Run `npm run build`.
1313

1414
## Run Mailer
1515
Just run `npm start`.

‎dist/index.js

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
const cors_1 = __importDefault(require("cors"));
7+
const dotenv_1 = __importDefault(require("dotenv"));
8+
const express_1 = __importDefault(require("express"));
9+
const http_1 = __importDefault(require("http"));
10+
const https_1 = __importDefault(require("https"));
11+
const mailer_1 = __importDefault(require("./mailer"));
12+
const whitelist_json_1 = __importDefault(require("../whitelist.json"));
13+
dotenv_1.default.config({ path: './config/.env' });
14+
const mailers = new Map();
15+
const app = (0, express_1.default)();
16+
app.use(express_1.default.json());
17+
app.use(express_1.default.urlencoded({ extended: true }));
18+
if (whitelist_json_1.default.length) {
19+
console.log('Use whitelist:', whitelist_json_1.default);
20+
app.use((0, cors_1.default)({
21+
origin: function (origin, callback) {
22+
if (origin && whitelist_json_1.default.indexOf(origin) !== -1) {
23+
callback(null, true);
24+
}
25+
else {
26+
callback(new Error('Not allowed by CORS'));
27+
}
28+
}
29+
}));
30+
}
31+
else {
32+
console.log('Whitelist not found, allow all origin.');
33+
app.use((0, cors_1.default)());
34+
}
35+
app.post('/send-mail', (req, res) => {
36+
const { account, from, to, subject, message, contentType } = req.body;
37+
if (!mailers.has(account))
38+
mailers.set(account, new mailer_1.default(account));
39+
const nMailSent = mailers.get(account).sendMail({ from, to, subject, message, contentType });
40+
res.send(nMailSent);
41+
});
42+
app.get('/mail-queue/:account', (req, res) => {
43+
const { account } = req.params;
44+
if (!account) {
45+
return res.status(400).send('Account is required');
46+
}
47+
if (!mailers.has(account))
48+
mailers.set(account, new mailer_1.default(account));
49+
res.json({
50+
result: mailers.get(account).getMailQueue(),
51+
});
52+
});
53+
app.get('/failed-mail/:account', (req, res) => {
54+
const { account } = req.params;
55+
if (!account) {
56+
return res.status(400).send('Account is required');
57+
}
58+
if (!mailers.has(account))
59+
mailers.set(account, new mailer_1.default(account));
60+
res.json({
61+
result: mailers.get(account).getFailedMails(),
62+
});
63+
});
64+
const HTTP_PORT = process.env.HTTP_PORT;
65+
const HTTPS_PORT = process.env.HTTPS_PORT;
66+
if (!HTTP_PORT && !HTTPS_PORT) {
67+
console.error('HTTP_PORT or HTTPS_PORT are required');
68+
process.exit(1);
69+
}
70+
if (HTTP_PORT) {
71+
http_1.default.createServer(app).listen(HTTP_PORT, () => {
72+
console.log(`http server listen: ${HTTP_PORT}`);
73+
});
74+
}
75+
if (HTTPS_PORT) {
76+
const options = {
77+
key: process.env.SSL_KEY,
78+
cert: process.env.SSL_CERT,
79+
};
80+
https_1.default.createServer(options, app).listen(HTTPS_PORT, () => {
81+
console.log(`https server listen: ${HTTPS_PORT} with options: ${JSON.stringify(options)}`);
82+
});
83+
}

‎dist/mailer.js

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
exports.getOAuth2Client = void 0;
7+
const promises_1 = require("fs/promises");
8+
const readline_1 = __importDefault(require("readline"));
9+
const googleapis_1 = require("googleapis");
10+
const SCOPES = [
11+
'https://mail.google.com/',
12+
'https://www.googleapis.com/auth/gmail.send',
13+
];
14+
const base64Encode = (message) => Buffer.from(message).toString('base64').replace(/\+/g, '-').replace(/\//g, '_');
15+
const getOAuth2Client = async (credentialsName, tokenName, noTokenInitialize = false) => {
16+
const credentialsPath = `credentials/credentials.${credentialsName}.json`;
17+
const tokenPath = `token/token.${tokenName}.json`;
18+
try {
19+
const content = await (0, promises_1.readFile)(credentialsPath);
20+
return await authorize(JSON.parse(content), tokenPath, noTokenInitialize);
21+
}
22+
catch (e) {
23+
console.error('Error on getOAuth2Client():', e);
24+
return null;
25+
}
26+
};
27+
exports.getOAuth2Client = getOAuth2Client;
28+
const authorize = async (credentials, tokenPath, noTokenInitialize = false) => {
29+
const { client_secret, client_id, redirect_uris } = credentials.installed;
30+
const oAuth2Client = new googleapis_1.google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
31+
try {
32+
const token = await (0, promises_1.readFile)(tokenPath);
33+
oAuth2Client.setCredentials(JSON.parse(token));
34+
return oAuth2Client;
35+
}
36+
catch (e) {
37+
if (noTokenInitialize) {
38+
throw 'Token file not found!';
39+
}
40+
return await getNewToken(oAuth2Client, tokenPath);
41+
}
42+
};
43+
const getNewToken = (oAuth2Client, tokenPath) => new Promise((resolve, reject) => {
44+
const authUrl = oAuth2Client.generateAuthUrl({
45+
access_type: 'offline',
46+
scope: SCOPES,
47+
});
48+
console.log('Authorize this app by visiting this url:', authUrl);
49+
const rl = readline_1.default.createInterface({
50+
input: process.stdin,
51+
output: process.stdout,
52+
});
53+
rl.question('Enter the code from that page here: ', code => {
54+
rl.close();
55+
oAuth2Client.getToken(code, async (err, token) => {
56+
if (err) {
57+
console.error('Error retrieving access token', err);
58+
return reject();
59+
}
60+
if (!token) {
61+
console.error('No token retrieved');
62+
return reject();
63+
}
64+
oAuth2Client.setCredentials(token);
65+
try {
66+
await (0, promises_1.writeFile)(tokenPath, JSON.stringify(token));
67+
console.log('Token stored to', tokenPath);
68+
return resolve(oAuth2Client);
69+
}
70+
catch (e) {
71+
console.error(e);
72+
return reject();
73+
}
74+
});
75+
});
76+
});
77+
class Mailer {
78+
#gmail = null;
79+
#account;
80+
#mailQueue;
81+
#failedMails;
82+
constructor(account) {
83+
this.#account = account;
84+
this.#mailQueue = [];
85+
this.#failedMails = [];
86+
this.initClient();
87+
}
88+
async initClient() {
89+
this.#gmail = null;
90+
const oAuth2Client = await (0, exports.getOAuth2Client)(this.#account, this.#account, true);
91+
if (oAuth2Client) {
92+
this.#gmail = googleapis_1.google.gmail({ version: 'v1', auth: oAuth2Client });
93+
this.sendMail();
94+
}
95+
}
96+
sendMail(mail) {
97+
if (mail)
98+
this.#mailQueue.push(mail);
99+
if (!this.#gmail)
100+
return 0;
101+
let nMailSent = 0;
102+
for (const mail of this.#mailQueue) {
103+
const { from, to, subject, message, contentType = 'text/plain' } = mail;
104+
this.#gmail.users.messages.send({
105+
userId: 'me',
106+
requestBody: {
107+
raw: base64Encode(`From: ${from}\n` +
108+
`To: ${to}\n` +
109+
`Subject: ${subject}\n` +
110+
'MIME-Version: 1.0\n' +
111+
`Content-Type: ${contentType}; charset="UTF-8"\n` +
112+
'Content-Transfer-Encoding: message/rfc2822\n' +
113+
'\n' +
114+
`${message}\n`),
115+
},
116+
}, (err, res) => {
117+
if (err) {
118+
this.#failedMails.push({ ...mail, failReason: err, timestamp: Date.now() });
119+
return console.log('The API returned an error: ' + err);
120+
}
121+
nMailSent++;
122+
console.log(`Message sent with ID: ${res?.data.id}`);
123+
});
124+
}
125+
this.#mailQueue = [];
126+
return nMailSent;
127+
}
128+
getMailQueue() {
129+
return this.#mailQueue;
130+
}
131+
getFailedMails() {
132+
return this.#failedMails;
133+
}
134+
}
135+
exports.default = Mailer;

‎dist/tokenInitializer.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use strict";
2+
var __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { "default": mod };
4+
};
5+
Object.defineProperty(exports, "__esModule", { value: true });
6+
const readline_1 = __importDefault(require("readline"));
7+
const mailer_1 = require("./mailer");
8+
const rl = readline_1.default.createInterface({
9+
input: process.stdin,
10+
output: process.stdout,
11+
});
12+
const prompt = () => {
13+
rl.question('Enter an account to initialize(Enter \'exit\' to exit): ', async (input) => {
14+
const account = input.trim();
15+
if (account) {
16+
if (account === 'exit') {
17+
rl.close();
18+
return;
19+
}
20+
await (0, mailer_1.getOAuth2Client)(account, account);
21+
console.log(`Token for ${account} initialized.`);
22+
}
23+
prompt();
24+
});
25+
};
26+
prompt();

‎package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
"main": "index.js",
66
"scripts": {
77
"test": "echo \"Error: no test specified\" && exit 1",
8-
"start": "npm run dev",
9-
"dev": "nodemon --exec ts-node src/index.ts",
10-
"init": "ts-node tokenInitializer.ts"
8+
"start": "nodemon --exec node dist/index.js",
9+
"build": "tsc",
10+
"init": "npm i && echo [] > whitelist.json && npm run init-token",
11+
"init-token": "node dist/tokenInitializer.js"
1112
},
1213
"repository": {
1314
"type": "git",

‎src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ app.use(express.json());
1616
app.use(express.urlencoded({ extended: true }));
1717

1818
// CORS
19-
if (whitelist?.length) {
19+
if (whitelist.length) {
2020
console.log('Use whitelist:', whitelist);
2121
app.use(cors({
2222
origin: function (origin, callback) {
23-
if (origin && whitelist.indexOf(origin) !== -1) {
23+
if (origin && (whitelist as string[]).indexOf(origin) !== -1) {
2424
callback(null, true);
2525
} else {
2626
callback(new Error('Not allowed by CORS'));

‎tokenInitializer.ts ‎src/tokenInitializer.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import readline from 'readline';
2-
import { getOAuth2Client } from './src/mailer';
2+
import { getOAuth2Client } from './mailer';
33

44
const rl = readline.createInterface({
55
input: process.stdin,
@@ -26,4 +26,3 @@ const prompt = () => {
2626
};
2727

2828
prompt();
29-

‎tsconfig.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
1212

1313
/* Language and Environment */
14-
"target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
14+
"target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
1515
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
1616
// "jsx": "preserve", /* Specify what JSX code is generated. */
1717
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
@@ -25,7 +25,7 @@
2525

2626
/* Modules */
2727
"module": "commonjs", /* Specify what module code is generated. */
28-
// "rootDir": "./", /* Specify the root folder within your source files. */
28+
"rootDir": "./src", /* Specify the root folder within your source files. */
2929
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
3030
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
3131
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
@@ -45,10 +45,10 @@
4545
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
4646
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
4747
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
48-
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
48+
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
4949
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
50-
// "outDir": "./", /* Specify an output folder for all emitted files. */
51-
// "removeComments": true, /* Disable emitting comments. */
50+
"outDir": "./dist", /* Specify an output folder for all emitted files. */
51+
"removeComments": true, /* Disable emitting comments. */
5252
// "noEmit": true, /* Disable emitting files from a compilation. */
5353
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
5454
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
@@ -74,7 +74,7 @@
7474

7575
/* Type Checking */
7676
"strict": true, /* Enable all strict type-checking options. */
77-
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
77+
"noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
7878
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
7979
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
8080
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */

‎whitelist.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
[
2-
]
1+
[]

0 commit comments

Comments
 (0)
Please sign in to comment.