使用Google邮件服务账户时Nodemailer调用sendmail()出现401错误
Let’s break down your questions and fix that frustrating 401 error step by step:
1. Is your JWT Client working correctly?
Your JWT client is initialized properly, and the authorize() call succeeded (you got tokens back), so the basic setup is valid. The critical missing piece here is that Service Accounts need explicit permission to act on behalf of a G Suite user (your contact@example.com email) via Domain-Wide Delegation. Without that, even a valid token won’t let you send emails through SMTP.
2. Are your tokens normal?
id_token: undefinedis totally fine—you’re requesting thehttps://mail.google.com/scope, which doesn’t return an ID token (that’s only for OpenID Connect scopes likeopenid).refresh_token: 'jwt-placeholder'is expected behavior for Service Account JWT auth. Unlike user-facing OAuth2, Service Accounts don’t get a real refresh token—instead, you can re-runjwtClient.authorize()to generate a new access token whenever the old one expires. Including this placeholder in your Nodemailer config isn’t harmful, but it’s unnecessary.
3. What does the "Invalid status code 401" error mean?
A 401 status code means Unauthorized. In this case, your SMTP request is being rejected for two key reasons:
- Your Service Account hasn’t been granted permission to send emails as
contact@example.com. - Your Nodemailer OAuth2 configuration is missing the
subjectfield, which tells Google which user the Service Account is impersonating.
Step-by-Step Fix
Prerequisite
You must be using a G Suite account (personal Gmail accounts don’t support Domain-Wide Delegation for Service Accounts).
1. Enable Domain-Wide Delegation for your Service Account
- Open your Google Cloud Console → Navigate to IAM & Admin → Service Accounts.
- Find your Service Account, click the three-dot menu → Edit.
- Check Enable G Suite Domain-wide Delegation and save.
- Copy the Client ID displayed (you’ll need this for the next step).
2. Grant Permissions in G Suite Admin Console
- Log into your G Suite Admin Console → Go to Security → API Controls → Domain-wide delegation.
- Click Add new → Paste the Client ID you copied earlier.
- In the OAuth scopes field, enter
https://mail.google.com/→ Click Authorize.
3. Update Your Code
Make these key changes to your function:
- Add the
subjectparameter to your JWT client initialization (this tells Google which user you’re impersonating). - Clean up your Nodemailer auth config (remove unnecessary fields like
refreshTokenandexpires, addsubject).
Here’s the revised code:
const functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp(); const { google } = require('googleapis'); const nodemailer = require('nodemailer'); exports.sendContactEmailOAuth = functions.https.onRequest((req, res) => { const sender_email = 'john.doe@acme.com'; const sender_msg = 'just a test to contact the site owner.'; const email = 'contact@example.com'; // The G Suite email you want to send from // Add "subject" to specify the user the Service Account will impersonate const jwtClient = new google.auth.JWT({ email: functions.config().service_key.client_email, key: functions.config().service_key.private_key, scopes: ['https://mail.google.com/'], subject: email }); console.log('JWT Client: ', jwtClient); jwtClient.authorize((error, tokens) => { if (error) { console.log('AUTHORIZATION FAILED?: ', error); res.json({ success: false, error: error }); return; } console.log('AUTHORIZED tokens: ', tokens); var transporter = nodemailer.createTransport({ host: "smtp.gmail.com", port: 465, secure: true, auth: { type: 'OAuth2', user: email, serviceClient: functions.config().service_key.client_id, privateKey: functions.config().service_key.private_key, accessToken: tokens.access_token, subject: email // Confirm the impersonated user here too // Remove refreshToken and expires—they're not needed for Service Accounts } }); console.log('nodemailer transporter set...'); const mailOptions = { from: `An Example <${sender_email}>`, to: email, subject: 'Apple and Banana', text: sender_msg, html: '<h1>Apple and Banana</h1><p>My html here</p>' }; transporter.sendMail(mailOptions, (error, response) => { if (error) { console.log(error); res.end('error'); } else { console.log("Message sent: " + response.messageId); // Fixed typo (was response.message) res.end('sent'); } transporter.close(); }); }); });
4. Verify
Deploy your updated Firebase function and test it again. The 401 error should disappear once the Service Account has permission to impersonate your G Suite email.
内容的提问来源于stack exchange,提问作者user762579

