I don't why i'm getting this every time I try to use Nodemailer to send emails:
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent
to the client
at new NodeError (node:internal/errors:371:5)
at ServerResponse.setHeader (node:_http_outgoing:576:11)
the thing is it's working but i'm getting this error, by working I mean It sends the email.
The Code:
const sendEmail = async (email, subject, payload, template) => {
try {
// create reusable transporter object using the default SMTP transport
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.EMAIL_USERNAME,
pass: process.env.EMAIL_PASSWORD,
},
});
const options = () => {
return {
from: process.env.FROM_EMAIL,
to: email,
subject: subject,
html: template,
};
};
// Send email
transporter.sendMail(options(), (error, info) => {
if (error) {
return error;
} else {
return res.status(200).json({
success: true,
});
}
});
} catch (error) {
throw error;
}
};
router.post("/", async (req, res, next) => {
try {
if (!req.body.newsletterEmail) {
return next(createError(400, "Please enter your email"));
} else if (!req.body.newsletterEmail || !isEmail(req.body.newsletterEmail)) {
return next(createError(400, "Please enter a valid email address"));
}
const checkEmail = await SubscribedEmails.findOne({ newsletterEmail: req.body.newsletterEmail });
if (checkEmail) {
return next(createError(400, "Email is already subscribed"));
} else {
//create new user
const newSubscribedEmail = new SubscribedEmails ({
newsletterEmail: req.body.newsletterEmail,
});
//save user and respond
const SubscribedEmail = await newSubscribedEmail.save();
res.status(200).json({
message: "Successfully subscribed",
SubscribedEmail,
});
const link = `${process.env.CLIENT_URL}/`;
await sendEmail(
SubscribedEmail.newsletterEmail,
"Welcome to our newsletter!",
{
link: link,
},
`<div> <p>Hi,</p> <p>You are subscribed to our.</p> <p> Please click the link below to view more</p> <a href=${link}>GO</a> </div>`
);
return res.status(200).send(link);
}
} catch(err) {
next(err);
}
});
This is likely due to some quirks with using Gmail with Nodemailer. Review the docs here - https://nodemailer.com/usage/using-gmail/
and configure your Gmail account here -
https://myaccount.google.com/lesssecureapps
Finally, you may run into issues with Captcha. Configure here - https://accounts.google.com/DisplayUnlockCaptcha
Related
I am trying to implement a mechanism to allow new users to verify their account with a link sent to their email address.
Here's is my subscribe route in routes/user/index.js, which is working to send the email correctly:
require('dotenv').config();
const { v4 } = require('uuid');
const nodemailer = require('nodemailer');
const sibApiV3Sdk = require('sib-api-v3-sdk');
const express = require('express');
const stripe = require('../middlewares/stripe')
const db = require("../models");
const cors = require('cors');
const User = db.user;
const Feedback = db.feedback;
const defaultClient = sibApiV3Sdk.ApiClient.instance;
// Configure API key authorization: api-key
const apiKey = defaultClient.authentications['api-key'];
apiKey.apiKey = process.env.SENDINBLUE_API_KEY;
const transactionalEmailsApi = new sibApiV3Sdk.TransactionalEmailsApi();
// Configure nodemailer
const transporter = nodemailer.createTransport({
host: "smtp-relay.sendinblue.com",
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: "noreply#redacted.ai",
pass: "redacted"
}
});
// Define generateVerificationToken function
function generateVerificationToken() {
return v4();
}
// Prepare Core Router
let app = express.Router()
app.post('/stripe/subscribe', async (req, res) => {
const domainURL = process.env.DOMAIN;
const { priceId, trial } = req.body
try {
let user = await User.findOne({ _id: req.user._id });
let customer = user.customerId ? { customer: user.customerId } : {customer_email: user.email};
const subscription_data = trial ? { trial_period_days: 7 } : {};
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
...customer,
line_items: [
{
price: priceId,
quantity: 1,
},
],
subscription_data,
success_url: `${domainURL}/index.html`,
cancel_url: `${domainURL}/signup/failed`,
});
let credits = 0;
if (priceId === process.env.STRIPE_PRODUCT_FREE) {
credits = 0;
} else if (priceId === process.env.STRIPE_PRODUCT_ENTRY) {
credits = 0;
} else if (priceId === process.env.STRIPE_PRODUCT_PRO) {
credits = 0;
}
// Check if user already has a verification_token
if (!user.verification_token) {
const token = generateVerificationToken();
console.log("generated token: ", token);
const updatedUser = await User.findOneAndUpdate({ _id: req.user._id },
{
credits: credits,
verification_token: token,
isVerified: false
},
{
new: true
});
console.log("updatedUser: ", updatedUser);
const verificationLink = `${domainURL}/verify?token=${updatedUser.verification_token}`;
const mailOptions = {
from: 'noreply#redacted.ai',
to: user.email,
subject: 'Verify your email address',
html: `Please click on the following link to verify your email address: ${verificationLink}`
};
transporter.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
} else {
console.log('User already has a verification token.');
}
const updateResult = await User.updateOne({_id: req.user._id}, {verification_token: user.verification_token}, {upsert: true});
console.log("updateResult: ", updateResult);
// Send verification email after stripe step completes
if (!user.isVerified) {
const verificationLink = `${domainURL}/verify?token=${user.verification_token}`;
const mailOptions = {
from: 'noreply#redacted.ai',
to: user.email,
subject: 'Verify your email address',
html: `Please click on the following link to verify your email address: ${verificationLink}`
};
transporter.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}
// Redirect user to the verify route
res.redirect(303, session.url);
} catch (e) {
res.status(400);
return res.send({
error: {
message: e.message,
}
});
}
});
Immediately after this, I then have a verify route:
app.get('/verify', cors(), async (req, res) => {
const { token } = req.query;
try {
// Find user with matching token
let user = await User.findOne({ verification_token: token });
if (!user) {
throw new Error('Invalid token');
}
// Update user verification status
user.isVerified = true;
const savedUser = await user.save();
// Send verification email
if (savedUser.isVerified) {
const mailOptions = {
from: 'noreply#redacted.ai',
to: savedUser.email,
subject: 'Email Verified!',
html: `Your email has been verified! Welcome to redacted.ai`
};
transporter.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}
// Redirect user to success page
res.redirect('/index.html#/verify/success');
} catch (e) {
res.status(400);
return res.send({
error: {
message: e.message,
}
});
}
});
I also have a file called src/verify.js to deal with things on the front-end:
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
function Verify(props) {
const [isVerified, setIsVerified] = useState(false);
const { token } = props.match.params;
const history = useHistory();
useEffect(() => {
// logic to process token and set isVerified to true/false
fetch(`/verify/${token}`)
.then(res => res.json())
.then(data => {
if (data.isVerified) {
setIsVerified(true);
}
});
}, [token]);
useEffect(() => {
// redirect to success page if isVerified is true
if (isVerified) {
history.push('/index.html#/verify/success');
}
}, [isVerified, history]);
return (
<div>
{isVerified ? (
<div>Your email has been verified! Welcome to redacted.ai</div>
) : (
<div>Invalid token.</div>
)}
</div>
);
}
export default Verify;
However, when the user clicks on the link in their email, the verify route does not initiate. Rather, nothing happens at all in the terminal except for profile refresh, and I'm not sure why.
As an aside, I can see in the database that the new user has a verification_token and isVerfied is set to false. That verification_token matches the one sent in the email.
I'm very new to Node JS so suspect I'm missing something obvious! Thanks so much in advance.
PS.
As a work around, I attempted to try a different approach, and created a form that the user could go to and copy and paste in their verification_token: the form looked like this:
import React, { useState } from 'react';
import TOOLS from './tools'
import config from './config'
import Header from './Header'
let baseURL = config.baseURL
const VerifyPage = () => {
const [verificationToken, setVerificationToken] = useState('');
const [error, setError] = useState('');
const [responseData, setResponseData] = useState({});
const handleSubmit = async (event) => {
event.preventDefault();
const token = window.localStorage.getItem('token');
console.log('Token: ', token);
console.log('Authorization header: ', `Bearer ${token}`);
// Make a request to your API to verify the token
try {
const response = await fetch(`${baseURL}user/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ verificationToken })
});
console.log('Request URL: ', `${baseURL}user/verify`);
console.log('Request body: ', JSON.stringify({ verificationToken }));
const responseJson = await response.json();
setResponseData(responseJson);
console.log('Response status: ', response.status);
console.log('Response text: ', await response.text());
if (!response.ok) {
throw new Error(responseJson.message);
}
// Show a success message or redirect to another page
} catch (error) {
setError(error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="verificationToken">Verification Token:</label>
<input
type="text"
id="verificationToken"
value={verificationToken}
onChange={(event) => setVerificationToken(event.target.value)}
/>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit">Verify</button>
{responseData && JSON.stringify(responseData)}
</form>
);
};
export default VerifyPage;
And with this in place, I altered my verify route to looks like this:
app.post("/verify", (req, res) =\> {
console.log('Received request to /verify');
const { token } = req.body;
console.log('Received verification token:', token);
User.findOne({ token }, (err, user) => {
if (err) {
console.error('Error finding user:', err);
return res.status(500).send({ error: "Error verifying account" });
}
if (!user) {
console.error('No user found with provided verification token');
return res.status(400).send({ error: "Invalid Token" });
}
user.isVerified = true;
user.verificationToken = undefined;
user.save((err) => {
if (err) {
console.error('Error saving user:', err);
return res.status(500).send({ error: "Error verifying account" });
}
console.log('User verified successfully');
return res.send({ message: "Account verified successfully" });
});
});
});`
But whenever the user attempted to put in their valid token, they would get something like this:
"Unexpected token 'F', "Forbidden" is not valid JSON"
Moreover, this is all that would show up in the terminal:
`"OPTIONS /api/user/verify 204 0.285 ms - 0 POST /api/user/verify 403 1.558 ms - 9" `
And, under the JWT code, this would show up in the console: "Failed to load resource: the server responded with a status of 403 (Forbidden)"
Truth be told, I'd be happy to get this working either via the form, or by having users click the link in the email. The latter is preferable - but either would work.
I'm trying to find a way to set up a contact us form, when a user is submitting an enquiry I should receive an email, also the user should receive an email telling that the enquiry reached us, I figured out that I should use nodemailer to send mails, now the first step is done which I receive an email whenever the user submitted an enquiry, the question is how can I send him an email after submitting the enquiry? excuse my bad English
Nodemailer setup
//email from the contact form using the nodemailer
router.post('/send', async(req, res) => {
console.log(req.body);
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USERNAME, //email resposible for sending message
pass: process.env.EMAIL_PASSWORD
}
});
const mailoptions = {
from: process.env.FROM_EMAIL,
to: process.env.EMAIL_USERNAME, //email that recives the message
subject: 'New Enquiry',
html: `<html><body><p>Name: ${req.body.name}</p><p> Email: ${req.body.email}</p><p>message: ${req.body.content}</p></body></html>`
}
transporter.sendMail(mailoptions, (err, info) => {
if (err) {
console.log('error');
res.send('message not sent'); //when its not ent
} else {
console.log('message sent' + info.response);
res.send('sent'); //when sent
}
})
});
module.exports = router;
So I tried some codes and code worked for me, it's sending to the admin and the client, hope it helps
router.post('/send', async(req, res, next) => {
console.log(req.body);
if (!req.body.name) {
return next(createError(400, "ERROR MESSAGE HERE"));
} else if (!req.body.email) {
return next(createError(400, "ERROR MESSAGE HERE"));
} else if (!req.body.email || !isEmail(req.body.email)) {
return next(createError(400, "ERROR MESSAGE HERE"));
} else if (!req.body.content) {
return next(createError(400, "ERROR MESSAGE HERE"));
}
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USERNAME, //email resposible for sending message
pass: process.env.EMAIL_PASSWORD
}
});
const mailOptionsAdmin = {
from: process.env.FROM_EMAIL,
to: process.env.EMAIL_USERNAME, //email that recives the message
subject: 'New Enquiry',
html: `<html><body><p>Name: ${req.body.name}</p><p> Email: ${req.body.email}</p><p>message: ${req.body.content}</p></body></html>`
}
const mailOptionsClient = {
from: process.env.FROM_EMAIL,
to: req.body.email, //email that recives the message
subject: 'Recivied',
html: `<html><body><p>message: We recived your enquiry</p></body></html>`
}
transporter.sendMail(mailOptionsAdmin, (err, info) => {
if (err) {
console.log('error');
res.send('message not sent'); //when its not ent
} else {
console.log('message sent' + info.response);
res.send('sent'); //when sent
transporter.sendMail(mailOptionsClient)
}
})
});
I was creating a contact form using nodemailer. I implemented it and it was working fine, but some time later I came back to do a final test in postman it started to give me the following error"message": Not Found - /contact and "stack": "Error: Not Found - /contact\n . I am not sure what's causing the error.
this is code:
const transport = {
host: 'smtp.gmail.com', // Don’t forget to replace with the SMTP host of your provider
port: 587,
secure: false, // use SSL
auth: {
user: process.env.GMAIL,
pass: process.env.PASSWORD
}
}
const transporter = nodemailer.createTransport(transport)
transporter.verify((error, success) => {
if (error) {
console.log(error);
} else {
console.log('Server is ready to take messages');
}
});
app.post('/contact', (req, res, next) => {
const name = req.body.name
const email = req.body.email
const message = req.body.message
const content = ` name: ${name} \n email: ${email} \n message: ${message} `
const mail = {
from: name,
// to: 'RECEIVING_EMAIL_ADDRESS_GOES_HERE', // Change to email address that you want to receive messages on
to: 'xxx#gmail.com', // Change to email address that you want to receive messages on
subject: 'New Message from Contact Form',
text: content
}
transporter.sendMail(mail, (err, data) => {
if (err) {
res.json({
status: 'fail'
})
} else {
res.json({
status: 'success'
})
}
})
})
In my Node.js/MERN app, I get error 250 2.0.0 OK 1590267554 o18sm275551eje.40 - gsmtp & something went wrong when I register my user for authentication and receive an email with EMPTY body. I am using the code from https://blog.bitsrc.io/email-confirmation-with-react-257e5d9de725
I can see user is added to mongodb database with confirmed set to false. Why am I not getting the complete email with confirmation?
Please find my attached code for my MERN application. I would really appreciate a reply! Thank you!
Register
Register route in users which takes you to login and on React side OnSubmit starts chain of sending and confirming email.
router.post("/register", type, function (req, res, next) {
// var tmp_path = req.file.path;
if(!req.file){
console.log("File missing");
}
/** The original name of the uploaded file
stored in the variable "originalname". **/
// var target_path = 'uploads/' + req.file.originalname;
// /** A better way to copy the uploaded file. **/
// var src = fs.createReadStream(tmp_path);
// var dest = fs.createWriteStream(target_path);
// src.pipe(dest);
// fs.unlink(tmp_path);
// src.on('end', function() { res.render('complete'); });
// src.on('error', function(err) { res.render('error'); });
// Form validation
const { errors, isValid } = validateRegisterInput(req.body);
const url = req.protocol + '://' + req.get('host')
// Check validation
if (!isValid) {
return res.status(400).json(errors);
}
//Checks email against registered emails in database
registeredemails.findOne({ email: req.body.email}).select("email").lean().then(result => {
if (!result) {
return res.status(400).json({email: "Email not provided"});
}
});
User.findOne({ email: req.body.email }).then(user =>
{
if (user) {return res.status(400).json({ email: "Email already exists" })
}
else if(!user){
const newUser = new User({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
password: req.body.password,
fileimg: url + '/public/' + req.file.filename
});
// // Hash password before saving in database
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (err, hash) => {
if (err) throw err;
newUser.password = hash;
newUser
.save()
.then(newUser =>
sendEmail(newUser.email),
templates.confirm(newUser._id)
)
.then(() => res.json({ msg: msgs.confirm }))
.catch(err => console.log(err))
}
)}
)}
else if (user && !user.confirmed) {
sendEmail(user.email, templates.confirm(user._id))
.then(() => res.json({ msg: msgs.resend })).catch(err => console.log(err))
}
// The user has already confirmed this email address
else {
res.json({ msg: msgs.alreadyConfirmed })
}
}).catch(err => console.log(err))
sendemail as used in the MEDIUM articles
const nodemailer = require('nodemailer');
const { CLIENT_ORIGIN } = require('../../config')
// const mg = require('nodemailer-mailgun-transport');
// The credentials for the email account you want to send mail from.
const credentials = {
secure: true,
service: 'Gmail',
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS
// These environment variables will be pulled from the .env file
// apiKey: 'b61286bf9e28b149fac32220f0c7349f-e5e67e3e-00a38515',
// domain: 'sandbox0b8a7f0ebcc74c0d8161304f24909bd2.mailgun.org'
}
}
// Getting Nodemailer all setup with the credentials for when the 'sendEmail()'
// function is called.
const transporter = nodemailer.createTransport(credentials)
// exporting an 'async' function here allows 'await' to be used
// as the return value of this function.
module.exports = async (to, content) => {
// The from and to addresses for the email that is about to be sent.
const contacts = {
from: process.env.MAIL_USER,
to // An array if you have multiple recipients.
// subject: 'React Confirm Email',
// html: `
// <a href='${CLIENT_ORIGIN}/confirm/${id}'>
// click to confirm email
// </a>
// `,
// text: `Copy and paste this link: ${CLIENT_ORIGIN}/confirm/${id}`
}
// Combining the content and contacts into a single object that can
// be passed to Nodemailer.
const email = Object.assign({}, content, contacts)
// This file is imported into the controller as 'sendEmail'. Because
// 'transporter.sendMail()' below returns a promise we can write code like this
// in the contoller when we are using the sendEmail() function.
//
// sendEmail()
// .then(() => doSomethingElse())
//
// If you are running into errors getting Nodemailer working, wrap the following
// line in a try/catch. Most likely is not loading the credentials properly in
// the .env file or failing to allow unsafe apps in your gmail settings.
await transporter.sendMail(email, function(error, info){
if(error)
{
return console.log(error);
}
else
{
return console.log(info.response);
}
})
}
templates.confirm as used in Medium article
onst { CLIENT_ORIGIN } = require('../../config')
// This file is exporting an Object with a single key/value pair.
// However, because this is not a part of the logic of the application
// it makes sense to abstract it to another file. Plus, it is now easily
// extensible if the application needs to send different email templates
// (eg. unsubscribe) in the future.
module.exports = {
confirm: id => ({
subject: 'React Confirm Email',
html: `
<a href='${CLIENT_ORIGIN}/confirm/${id}'>
click to confirm email
</a>
`,
text: `Copy and paste this link: ${CLIENT_ORIGIN}/confirm/${id}`
})
}
I can't cancel the request after sending the email successfully. How can I return properly after sending the email? This might be a callback hell, but I cant figure out how to solve it.
I tried to put some return in different parts but it didn't work.
const router = require('express').Router();
const nodemailer = require('nodemailer');
const emailExistence= require('email-existence');
module.exports = router;
// Send email when user has forgotten his/her password
router.post('/forgetPass', (req, res, next) => {
if(!req.body.email){
next(new Error("Email is required."));
return;
}
emailExistence.check(req.body.email, function(err,res){
if(err || !res){
next(new Error("The email does'nt exist."));
return;
}else{
let transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'myemail#gmail.com',
pass: 'mypassword'
}
});
let mailOptions = {
from: 'myemail#gmail.com',
to: req.body.email,
subject: 'Link for setting a new password',
html: 'Set a new password'
text: 'email text'
};
transporter.sendMail(mailOptions, function(error, info){
if (error) {
next(new Error("Error in sending email."));
return;
}
res.json(Object.assign(req.base, {
message: "The email has been sent successfully.",
data: info
}));
return;
});
}
});
});
Once you set your response field on successful sending, call next() as a last step, so the next middleware gets the request and sends the response back. So basically:
...
res.json(yourResponse);
next();
...
Or, if this is the last middleware, send the response back to client:
res.send(yourResponse);
I solved it in this way. The emailExistence didn't let me to use promises, so I used email-ckeck instead of it:
const router = require('express').Router();
const nodemailer = require('nodemailer');
const emailExistence= require('email-existence');
var emailCheck = require('email-check');
module.exports = router;
router.post('/forgetPass', (req, res, next) => {
if(!req.body.email){
next(new Error("Email is required."));
return;
}
// Check the req.body.email with email pattern regex
var patt = new RegExp (process.env.EMAIL_PATTERN__REGEX),
isEmail = patt.test(req.body.email);
if(!isEmail){
next(new Error("The email does'nt seem to be a valid email. If you are sure about your email validity contact the website admin."));
return;
}
return emailCheck(req.body.email)
.then(function(result){
let transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL,
pass: process.env.EMAILPASSWORD
}
});
let mailOptions = {
from: process.env.EMAIL,
to: req.body.email,
subject: 'Link for setting a new password',
html: 'Set a new password from this link.'
};
return transporter.sendMail(mailOptions)
.then(function (result2) {
res.status(200).json(Object.assign(req.base, {
message: "The email has been sent successfully.",
data: null
}));
return;
},
function(error2){
next(new Error("Error in sending email."));
return;
});
},
function(error) {
next(new Error("The email does'nt seem to be a valid email. If you are sure about your email validity contact the website admin."));
return;
});
});