Skip to content

Commit

Permalink
fix(firestore-send-email): replaced nodemailer-sendgrid as vulnerable
Browse files Browse the repository at this point in the history
  • Loading branch information
CorieW committed Oct 1, 2024
1 parent b908489 commit 473b84b
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 20 deletions.
82 changes: 71 additions & 11 deletions firestore-send-email/functions/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions firestore-send-email/functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"author": "",
"license": "Apache-2.0",
"dependencies": {
"@sendgrid/mail": "^8.1.3",
"@types/express-serve-static-core": "4.17.30",
"@types/node": "^20.10.3",
"@types/nodemailer": "^6.2.1",
Expand Down
15 changes: 6 additions & 9 deletions firestore-send-email/functions/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createTransport } from "nodemailer";
import { URL } from "url";
import { invalidTlsOptions, invalidURI } from "./logs";
import { Config } from "./types";
import transports, { SendGridTransportOptions } from "./transports";

/**
* Utility function to compile a URL object
Expand Down Expand Up @@ -111,13 +112,9 @@ export function setSmtpCredentials(config: Config) {
export function setSendGridTransport(config: Config) {
const { smtpPassword } = config;

return createTransport({
host: "smtp.sendgrid.net",
port: 587,
auth: {
user: "apikey",
pass: smtpPassword,
},
tls: parseTlsOptions(config.tls),
});
const options: SendGridTransportOptions = {
apiKey: smtpPassword,
};

return createTransport(transports.sendGrid(options));
}
157 changes: 157 additions & 0 deletions firestore-send-email/functions/src/transports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Transport } from "nodemailer";
import * as sgMail from '@sendgrid/mail';

interface Mail {
normalize(callback: (err: Error | null, source?: any) => void): void;
}

export interface SendGridTransportOptions {
apiKey?: string;
}

class SendGridTransport implements Transport<SendGridTransportOptions> {
options: SendGridTransportOptions;
name: string;
version: string;

constructor(options: SendGridTransportOptions) {
this.options = options || {};
this.name = 'SendGrid';
this.version = undefined;
if (options.apiKey) {
sgMail.setApiKey(options.apiKey);
}
}

send(mail: Mail, callback: (err: Error | null, info: SendGridTransportOptions) => void) {
mail.normalize((err, source) => {
if (err) {
return callback(err, this.options);
}

// Format the message
let msg: any = {};
Object.keys(source || {}).forEach(key => {
switch (key) {
case 'subject':
case 'text':
case 'html':
msg[key] = source[key];
break;
case 'from':
case 'replyTo':
// Always convert the source to an array of similar objects:
// 1. If it's an object, wrap it in an array
// 2. If it's already an array, keep it as is
// 3. If it's null or undefined, make it an empty array
// Then, take the first item from the array
msg[key] = []
.concat(source[key] || [])
.map((entry: any) => ({
name: entry.name,
email: entry.address
}))
.shift();
break;
case 'to':
case 'cc':
case 'bcc':
// Same as above comment, but keep the array
msg[key] = [].concat(source[key] || []).map((entry: any) => ({
name: entry.name,
email: entry.address
}));
break;
case 'attachments':
{
// Map over the source attachments array and transform each entry
let attachments = source.attachments.map((entry: any) => {
// Create an attachment object with content, filename, type, and default to 'attachment' disposition
let attachment: any = {
content: entry.content,
filename: entry.filename,
type: entry.contentType,
disposition: 'attachment' // default disposition for regular attachments
};

// If the attachment has a content ID (cid), add it and set the disposition to 'inline'
if (entry.cid) {
// add property
attachment.content_id = entry.cid; // Adding content ID for inline attachments
attachment.disposition = 'inline'; // Inline attachments are typically images displayed in the email body
}

// Return the transformed attachment object
return attachment;
});

msg.attachments = [].concat(msg.attachments || []).concat(attachments);
}
break;
case 'alternatives':
{
let alternatives = source.alternatives.map((entry: any) => {
let alternative = {
content: entry.content,
type: entry.contentType
};
return alternative;
});

msg.content = [].concat(msg.content || []).concat(alternatives);
}
break;
case 'icalEvent':
{
let attachment = {
content: source.icalEvent.content,
filename: source.icalEvent.filename || 'invite.ics',
type: 'application/ics',
disposition: 'attachment'
};
msg.attachments = [].concat(msg.attachments || []).concat(attachment);
}
break;
case 'watchHtml':
{
let alternative = {
content: source.watchHtml,
type: 'text/watch-html'
};
msg.content = [].concat(msg.content || []).concat(alternative);
}
break;
case 'normalizedHeaders':
msg.headers = msg.headers || {};
Object.keys(source.normalizedHeaders || {}).forEach(header => {
msg.headers[header] = source.normalizedHeaders[header];
});
break;
case 'messageId':
msg.headers = msg.headers || {};
msg.headers['message-id'] = source.messageId;
break;
default:
msg[key] = source[key];
}
});

if (msg.content && msg.content.length) {
if (msg.text) {
msg.content.unshift({ type: 'text/plain', content: msg.text });
delete msg.text;
}
if (msg.html) {
msg.content.unshift({ type: 'text/html', content: msg.html });
delete msg.html;
}
}

sgMail.send(msg, null, (err) => { callback(err, this.options); });
});
}
}

export default {
sendGrid: (options: SendGridTransportOptions) => new SendGridTransport(options)
}

0 comments on commit 473b84b

Please sign in to comment.