How can I use nodemailer with Cloud Functions for Firebase? - firebase

I'm trying to use nodemailer in a Cloud Functions for Firebase but keep getting errors seeming to be that the smpt server cant be reached or found. Iv'e tried gmail, outlook and a normal hosted smpt service. It works well from my local node server.
This is the logged error I receive from the failed attempt to send email:
{
Error: getaddrinfoENOTFOUNDsmtp-mail.outlook.comsmtp-mail.outlook.com: 587aterrnoException(dns.js: 28: 10)atGetAddrInfoReqWrap.onlookup[
asoncomplete
](dns.js: 76: 26)code: 'ECONNECTION',
errno: 'ENOTFOUND',
syscall: 'getaddrinfo',
hostname: 'smtp-mail.outlook.com',
host: 'smtp-mail.outlook.com',
port: '587',
command: 'CONN'
}

I created a cloud function (http event) to send emails from the contact form section of my site with the following way:
const functions = require('firebase-functions');
const nodemailer = require('nodemailer');
const rp = require('request-promise');
//google account credentials used to send email
const mailTransport = nodemailer.createTransport(
`smtps://user#domain.com:password#smtp.gmail.com`);
exports.sendEmailCF = functions.https.onRequest((req, res) => {
//recaptcha validation
rp({
uri: 'https://recaptcha.google.com/recaptcha/api/siteverify',
method: 'POST',
formData: {
secret: 'your_secret_key',
response: req.body['g-recaptcha-response']
},
json: true
}).then(result => {
if (result.success) {
sendEmail('recipient#gmail.com', req.body).then(()=> {
res.status(200).send(true);
});
}
else {
res.status(500).send("Recaptcha failed.")
}
}).catch(reason => {
res.status(500).send("Recaptcha req failed.")
})
});
// Send email function
function sendEmail(email, body) {
const mailOptions = {
from: `<noreply#domain.com>`,
to: email
};
// hmtl message constructions
mailOptions.subject = 'contact form message';
mailOptions.html = `<p><b>Name: </b>${body.rsName}</p>
<p><b>Email: </b>${body.rsEmail}</p>
<p><b>Subject: </b>${body.rsSubject}</p>
<p><b>Message: </b>${body.rsMessage}</p>`;
return mailTransport.sendMail(mailOptions);
}

Related

Nodemailer in vercel not sending email in production

I'm using Nodemailer to send emails in my serverless Next.js project, deployed in Vercel, which works perfectly in development mode. But I'm having problems in production. No error returned, everything works the same way as is development mode, except I don't receive any email.
I have another project built with React and deployed in Heroku where I send emails the same way and it works fine, development and production, so I understand the problem is with Vercel.
Yes, I enabled "Allow Less Secured Apps" in Google account and yes, I enabled Captcha.
I also read this https://vercel.com/docs/solutions/email but it doesn't really make me understand what I should do in my case. I can see it's a matter of SMTP but I don't know what exactly.
Anybody experienced this kind of problem? How can I fix this?
const transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 465,
auth: {
user: myEmail#gmail.com,
pass: myEmailPass
}
});
const mailOptions = {
from: `${req.body.name} ${req.body.email}`,
to: myEmail#gmail.com,
subject: `${req.body.subject}`,
text: `Text: ${req.body.text}`
}
transporter.sendMail(mailOptions, (err, res) => {
if(err) {
console.log(err);
} else {
console.log("success");
}
});
UPDATE
I changed to SendGrid: made an account, created an API Key, and changed the code like so(instead the one above):
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: `myEmail#gmail.com`,
from: `myEmail#gmail.com`,
subject: `${req.body.subject}`,
text: `${req.body.text}`
};
sgMail
.send(msg)
.then(() => {
console.log('email sent')
})
.catch((error) => {
console.error("error", error)
});
It logs out "email sent" but I don't receive any email.
It's the same problem like with Nodemailer.
I'm confused now...
I ran into this issue and managed to fix it and keep using nodemailer by adding in promises with async/await.
const nodemailer = require("nodemailer");
export default async (req, res) => {
const { firstName, lastName, email, message } = JSON.parse(req.body);
const transporter = nodemailer.createTransport({
port: 465,
host: "smtp.gmail.com",
auth: {
user: "myEmail#gmail.com",
pass: "password",
},
secure: true,
});
await new Promise((resolve, reject) => {
// verify connection configuration
transporter.verify(function (error, success) {
if (error) {
console.log(error);
reject(error);
} else {
console.log("Server is ready to take our messages");
resolve(success);
}
});
});
const mailData = {
from: {
name: `${firstName} ${lastName}`,
address: "myEmail#gmail.com",
},
replyTo: email,
to: "recipient#gmail.com",
subject: `form message`,
text: message,
html: `${message}`,
};
await new Promise((resolve, reject) => {
// send mail
transporter.sendMail(mailData, (err, info) => {
if (err) {
console.error(err);
reject(err);
} else {
console.log(info);
resolve(info);
}
});
});
res.status(200).json({ status: "OK" });
};
This problem is really confusing indeed. I've managed to fix this by simply adding async/await. This is because streaming responses (fire-and-forget functions) are not supported by Vercel.
Source: https://vercel.com/docs/platform/limits#streaming-responses
I have already encountered the same problem, nodemailer was not working on vercel but on heroku everything worked perfectly. it is specified in the doc that vercel does not block stmp connections but according to what I have experienced, in practice stmp connections are blocked. what you can do is use an alternative to nodemailer. use sendgrid and it works fine
An article on how integrating Sendgrid with Next.js
I had a similar issue with Nodemailer but I fixed it by first adding the environment variables in Vercel then commit to the github(It will automatically be uploaded on vercel). So add the variables to vercel first for it to take effect
In my own case, wrapping my email function with async solved it for me.
eg:
const sendMessage = async(message)=>{
await transporter.sendMail({...options here})
}
Then in my API I called my function using:
await sendMessage('your message')
I tried all the async/await responses and didn't work at the beginning. Digging through the real time functions logs of the app, I noticed that there was an Error: Missing credentials for "PLAIN", so all I had to do was add the respective .env variables to vercel environment variables and it worked. Here's the complete code though:
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = any
const nodemailer = require('nodemailer')
const auth = {
user: process.env.WEB_MAILER,
pass: process.env.WEB_MAILER_PASSWORD,
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const { name, email, subject, message } = req.body
const mailData = {
to: process.env.EMAIL_TO,
from: process.env.WEB_MAILER,
name: name,
subject: subject,
text: `Email: ${email}.\n\nMessage: ${message}`,
html: `<div>Email: ${email}.\n\nMessage: ${message}</div>`,
}
const transporter = nodemailer.createTransport({
host: 'smtp.titan.email',
secure: true,
port: 465,
auth: auth,
})
const server = await new Promise((resolve, reject) => {
// verify connection configuration
transporter.verify(function (error: any, success: any) {
if (success) {
resolve(success)
}
reject(error)
})
})
if (!server) {
res.status(500).json({ error: 'Error failed' })
}
const success = await new Promise((resolve, reject) => {
// send mail
transporter.sendMail(mailData).then((info: any, err: any) => {
if (info.response.includes('250')) {
resolve(true)
}
reject(err)
})
})
if (!success) {
res.status(500).json({ error: 'Error sending email' })
}
res.status(200).json({ success: success })
}

Send email via firebase functions with hostgator accounts

I am having trouble sending emails through firebase functions, the mail account is from hostgator, I have done tests locally and it sends it without problem but when I upload the function to firebase the emails do not reach the destination and in the logs there are no errors, what could be happening?
The configuration is as follows:
var transporter = nodemailer.createTransport ({
host: ‘mail.example.com’,
port: 587,
secure: false,
auth: {
user: 'info#example.com’,
pass: ‘password’
},
tls: {
ciphers: ‘SSLv3’
},
debug: true
});
I have even tried other settings.
You can try this following snipet:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const nodemailer = require('nodemailer');
const cors = require('cors')({origin: true});
admin.initializeApp();
/**
* Here we're using Gmail to send
*/
let transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'yourgmailaccount#gmail.com',
pass: 'yourgmailaccpassword'
}
});
exports.sendMail = functions.https.onRequest((req, res) => {
cors(req, res, () => {
// getting dest email by query string
const dest = req.query.dest;
const mailOptions = {
from: 'Your Account Name <yourgmailaccount#gmail.com>', // Something like: Jane Doe <janedoe#gmail.com>
to: dest,
subject: 'I\'M A PICKLE!!!', // email subject
html: `<p style="font-size: 16px;">Pickle Riiiiiiiiiiiiiiiick!!</p>
<br />
<img src="https://images.prod.meredith.com/product/fc8754735c8a9b4aebb786278e7265a5/1538025388228/l/rick-and-morty-pickle-rick-sticker" />
` // email content in HTML
};
// returning result
return transporter.sendMail(mailOptions, (erro, info) => {
if(erro){
return res.send(erro.toString());
}
return res.send('Sended');
});
});
});
I have taken it from this page where you can find the whole tutorial.
Try this, you can see on the cPanel email configs:
let mailerConfig = {
service: "mail.DOMAIN.com",
host: "mail.DOMAIN.com",
secureConnection: true,
port: 465,
auth: {
user: EMAIL,
pass: PASSWORD,
},
};
let transporter = nodemailer.createTransport(mailerConfig);
Also, you can try to configure the firbase trigger:
Firebase Email Trigger

Authenticate Firebase with Auth0 using Netlify Lambda Functions

I have a web app built with Gatsby that has client-side authentication through Auth0. I want to use Firebase as a database for my project, but I need to authenticate users first before they can read/write to Firebase.
The Firebase SDK (firebase-admin) has a function called signInWithCustomToken(token) that I thought I could pass the token from Auth0 into, but this doesn't work (see: https://community.auth0.com/t/react-auth0-firebase/11392).
Instead, I need to proxy Auth0's token through an API which will use firebase-admin to issue a token. Because my Gatsby site is hosted on Netlify, I'm planning to use Netlify Lambda Functions to get proxy Auth0's token. This is where I'm getting stuck.
I've followed this tutorial on how to use Netlify Lambda Functions with Gastsby: https://www.gatsbyjs.org/blog/2018-12-17-turning-the-static-dynamic/
I then went into my Auth.js file where my Auth0 code is and dropped a fetch call in the setSession. I passed the idToken from Auth0 into the url in the fetch function. I'm not sure if this is the right thing to do. I've read in the tutorial that it would be passed in an authorization header, but I'm unclear what that means. Anyways, here's the complete auth.js file:
import auth0 from 'auth0-js';
const windowGlobal = typeof window !== 'undefined' && window;
class Auth {
auth0 = new auth0.WebAuth({
domain: process.env.Auth_Domain,
clientID: process.env.Auth_ClientId,
redirectUri: process.env.Auth_Callback,
responseType: 'token id_token',
scope: 'openid profile email',
});
constructor() {
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
}
login() {
this.auth0.authorize();
}
logout() {
// Remove the locally cached profile to avoid confusing errors.
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('expires_at');
localStorage.removeItem('user');
windowGlobal.window.location.replace(`https://login.skillthrive.com/v2/logout/?returnTo=http%3A%2F%2Flocalhost:8000`)
}
handleAuthentication() {
if (typeof window !== 'undefined') {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult)
} else if (err) {
console.log(err);
}
});
}
}
isAuthenticated() {
const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
return new Date().getTime() < expiresAt;
}
setSession(authResult) {
const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
localStorage.setItem('access_token', authResult.accessToken);
localStorage.setItem('id_token', authResult.idToken);
localStorage.setItem('expires_at', expiresAt);
fetch(`/.netlify/functions/firebase?id=${authResult.idToken}`)
.then(response => console.log(response))
this.auth0.client.userInfo(authResult.accessToken, (err, user) => {
localStorage.setItem('user', JSON.stringify(user));
})
}
getUser() {
if (localStorage.getItem('user')) {
return JSON.parse(localStorage.getItem('user'));
}
}
getUserName() {
if (this.getUser()) {
return this.getUser().name;
}
}
}
export default Auth;
I found a tutorial called How to Authenticate Firebase and Angular with Auth0 that has a function that mints a token for Firebase:
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const firebaseAdmin = require('firebase-admin');
// Config
const config = require('./config');
module.exports = function(app) {
// Auth0 athentication middleware
const jwtCheck = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${config.AUTH0_DOMAIN}/.well-known/jwks.json`
}),
audience: config.AUTH0_API_AUDIENCE,
issuer: `https://${config.AUTH0_DOMAIN}/`,
algorithm: 'RS256'
});
// Initialize Firebase Admin with service account
const serviceAccount = require(config.FIREBASE_KEY);
firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(serviceAccount),
databaseURL: config.FIREBASE_DB
});
app.get('/auth/firebase', jwtCheck, (req, res) => {
// Create UID from authenticated Auth0 user
const uid = req.user.sub;
// Mint token using Firebase Admin SDK
firebaseAdmin.auth().createCustomToken(uid)
.then(customToken =>
// Response must be an object or Firebase errors
res.json({firebaseToken: customToken})
)
.catch(err =>
res.status(500).send({
message: 'Something went wrong acquiring a Firebase token.',
error: err
})
);
});
I tried to incorporate small parts at a time into my Lambda function:
var admin = require("firebase-admin");
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
// For more info, check https://www.netlify.com/docs/functions/#javascript-lambda-functions
export function handler(event, context, callback) {
console.log("queryStringParameters", event.queryStringParameters);
const jwtCheck = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${process.env.Auth_Domain}/.well-known/jwks.json`
}),
audience: process.env.Auth_Audience,
issuer: `https://${process.env.Auth_Domain}/`,
algorithm: 'RS256'
});
callback(null, {
// return null to show no errors
statusCode: 200, // http status code
body: JSON.stringify({
msg: "Hello, World! " + Math.round(Math.random() * 10),
}),
})
}
I tried checking to see what came back for jwtCheck by console logging it, but all I got was something weird { [Function: d] unless: [Function], UnauthorizedError: [Function: r] }
How should I go about incorporating this into my Lambda function?
I found a module called serverless-http that allows me to write Lambda Function as if it were written in Express. This made it easy for me to wrap my head around what was happening, so I finally got this code to return the new minted token from Firebase:
const express = require('express');
const serverless = require('serverless-http');
const cors = require('cors');
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const firebaseAdmin = require('firebase-admin');
const app = express();
app.use(cors());
const jwtCheck = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `${process.env.Auth_Domain}/.well-known/jwks.json`
}),
audience: `${process.env.Auth_ClientId}`,
issuer: `${process.env.Auth_Domain}`,
algorithm: 'RS256'
});
const serviceAccount = require('../firebase/firebase-keys.json');
firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(serviceAccount),
databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`
});
// GET object containing Firebase custom token
app.get('/firebase', jwtCheck, async (req, res) => {
const {sub: uid} = req.user;
try {
const firebaseToken = await firebaseAdmin.auth().createCustomToken(uid);
res.json({firebaseToken});
} catch (err) {
res.status(500).send({
message: 'Something went wrong acquiring a Firebase token.',
error: err
});
}
});
module.exports.handler = serverless(app);
Then on the client side I wrapped the fetch call into a function like this and used it when needed:
async setFirebaseCustomToken() {
const response = await fetch('/.netlify/functions/firebase', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('id_token')}`,
},
});
const data = await response.json();
console.log(data.firebaseToken);
}
This code is just going to console.log the new token, but now you'll have the response to do what you want with in Firebase client-side. Hope this helps!

OAuth2 fails to return auth token using simple-oauth2 and Firebase Functions for Spotify Authentication

I have been working on a oauth2 flow for spotify by following this similar tutorial by the Firebase team for Instagram HERE
I am able to submit my credentials and return the user code and state in the url, but when I run the method to submit the code to return an auth token, the auth token that I print to console in the Firebase functions returns: Auth Token Error Not Found. Here's my workflow:
Here's the Spotify docs
FIRST, I have a function to configure my spotifyOAuth:
function spotifyOAuth2Client() {
// Spotify OAuth 2 setup
const credentials = {
client: {
id: functions.config().spotify.clientid,
secret: functions.config().spotify.clientsecret,
},
auth: {
tokenHost: 'https://accounts.spotify.com',
authorizePath: '/authorize'
},
};
return require('simple-oauth2').create(credentials);
}
I use that function in this Firebase function that is called using https://us-central1-<my project string>.cloudfunctions.net/redirect:
exports.redirect = functions.https.onRequest((req, res) => {
const oauth2 = spotifyOAuth2Client();
cookieParser()(req, res, () => {
const state = req.cookies.state || crypto.randomBytes(20).toString('hex');
console.log('Setting verification state:', state);
res.cookie('state', state.toString(), {
maxAge: 3600000,
secure: true,
httpOnly: true,
});
const redirectUri = oauth2.authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI,
//scope: OAUTH_SCOPES,
state: state,
});
console.log('Redirecting to:', redirectUri);
res.redirect(redirectUri);
});
});
The code above returns a url string with the proper parameters, the following code block is where my code breaks, I have another cloud function that runs after being redirected from the res.redirect(redirectUri) above. And when I try to run the getToken() method, it appears to not return anything because I hit the catch block instead? This is where I observe the Auth Token Error Not Found.
const oauth2 = spotifyOAuth2Client();
try {
return cookieParser()(req, res, async () => {
console.log('Received verification state:', req.cookies.state);
console.log('Received state:', req.query.state);
if (!req.cookies.state) {
throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.');
} else if (req.cookies.state !== req.query.state) {
throw new Error('State validation failed');
}
console.log('Received auth code:', req.query.code);
console.log(OAUTH_REDIRECT_URI);
// Get the access token object (the authorization code is given from the previous step).
const tokenConfig = {
code: req.query.code,
redirect_uri: 'http://localhost:8100/popup'
};
// Save the access token
try {
const result = await oauth2.authorizationCode.getToken(tokenConfig)
const accessToken = oauth2.accessToken.create(result);
console.log('inside try');
console.log(result);
console.log(accessToken);
} catch (error) {
console.log('Access Token Error', error.message);
}
I've double checked my spotify client/secret credentials in the config, what is going wrong with this OAuth2 flow?
Resolved my issue, I was not using the correct endpoints:
const credentials = {
client: {
id: functions.config().spotify.clientid,
secret: functions.config().spotify.clientsecret,
},
auth: {
tokenHost: 'https://accounts.spotify.com',
authorizePath: '/authorize',
tokenPath: '/api/token'
},
};

Unable to send SMS through Twilio and Google Functions

I am attempting to send a text (a one-time pass code) using Twilio, firebase and Google Functions, and using Postman.
I have run $ npm install --save twilio#3.0.0 -rc.13 in the functions directory.
When I run $ firebase deploy, it completes. But on Postman, when I do POST, Body and feed a JSON { "phone": "555-5555" }, I get an "Error: could not handle the request."
I am able to send a text in Twilio Programmable SMS from my Twilio number to an actual outside number direct to the mobile phone. I'm using live credentials for Sid and AuthToken.
Is this an issue with Twilio, Google Functions and some configurations?
Here are the logs on Functions:
// White flag sign//
Function execution took 1452 ms, finished with status: 'crash'
//Red Warning sign//
TypeError: handler is not a function
at cloudFunction (/user_code/node_modules/firebase-functions/lib/providers/https.js:26:41)
at /var/tmp/worker/worker.js:676:7
at /var/tmp/worker/worker.js:660:9
at _combinedTickCallback (internal/process/next_tick.js:73:7)
at process._tickDomainCallback (internal/process/next_tick.js:128:9)
Also, the google eslint forces consistent-return, which is why I put "return;" in the request-one-time-password.js. I cannot seem to turn it off by adding "consistent-return": 0 in eslintrc.
My code(with secret keys and phone numbers redacted):
//one-time-password/functions/service_account.js
has my keys copied and pasted.
//one-time-password/functions/twilio.js
const twilio = require('twilio');
const accountSid = 'redacted';
const authToken = 'redacted';
module.exports = new twilio.Twilio(accountSid, authToken);
//one-time-password/functions/request-one-time-password.js
const admin = require('firebase-admin');
const twilio = require('./twilio');
module.export = function(req, res) {
if(!req.body.phone) {
return res.status(422).send({ error: 'You must provide a phone number!'});
}
const phone = String(req.body.phone).replace(/[^\d]/g, '');
admin.auth().getUser(phone).then(userRecord => {
const code = Math.floor((Math.random() * 8999 + 1000));
// generate number between 1000 and 9999; drop off decimals
twilio.messages.create({
body: 'Your code is ' + code,
to: phone,
from: '+redacted'
}, (err) => {
if (err) { return res.status(422).send(err); }
admin.database().ref('users/' + phone).update({ code: code, codeValid: true }, () => {
res.send({ success: true });
})
});
return;
}).catch((err) => {
res.status(422).send({ error: err })
});
}
/////////////////////////////////
//one-time-password/functions/index.js
const admin = require('firebase-admin');
const functions = require('firebase-functions');
const createUser = require('./create_user');
const serviceAccount = require('./service_account.json')
const requestOneTimePassword = require('./request_one_time_password');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://one-time-password-650d2.firebaseio.com"
});
exports.createUser = functions.https.onRequest(createUser);
exports.requestOneTimePassword =
functions.https.onRequest(requestOneTimePassword);
You have
module.exports = new twilio.Twilio(accountSid, authToken);
on one line, and further down
module.export = function(req, res) { ... }.
Try changing export to exports.
One thing that tripped me up for a long time was how twilio sent the request body to the cloud function. It sends it in a body object so to access your request body it will look something like this
req.body.body
On top of that it passed it as a JSON string so I had to JSON.parse()
Example I got working:
export const functionName= functions.https.onRequest((req, res) => {
cors(req, res, () => {
let body = JSON.parse(req.body.body);
console.log(body);
console.log(body.service_id);
res.send();
});
});
This also may depend on the Twilio service you are using. I was using their Studio HTTP Request Module.
Hope this helps a little, not sure if it was your exact problem though :(

Resources