Ghost Debugging 4: SMTP Email Bug
Previously in Ghost Debugging 3: Stepping through code, we saw how to set break points at key stages of application initialization.
In this post we will set a breakpoint where the email error is being generated and step around the code to see why info@nodeholder.com
is getting replaced with noreply@localhost
.
Finding the error
In Ghost 5.47.1 the subscribe button does not work because Ghost fails to send a message to the SMTP server configured in /var/www/ghost/ config.production.json
, discussed on the forum here.
Debug Information:
OS: Ubuntu, v20.04.4 LTS
Node Version: v16.15.0
Ghost Version: 5.47.1
Ghost-CLI Version: 1.21.0
Environment: production
Command: 'ghost doctor'
This is NodeJS log message that handles the authorize by email request:
Jul 09 03:06:26 ghost-sfo2-01 node[137344]: [2023-07-09 10:06:26] INFO "POST /members/api/send-magic-link/" 400 901ms
This is the stack leading to the error:
- createMailError() - SMTPConnection._formatError() - SMTPConnection._actionRCPT() - SMTPConnection.() - SMTPConnection._processResponse() - SMTPConnection._onData() - TLSSocket.SMTPConnection._onSocketData()
All recipients were rejected
Reason: Can't send mail - all recipients were rejected: 553 5.7.1 <noreply@notes.nodeholder.com>: Sender address rejected: not owned by user info@nodeholder.com.
The SMTP server is saying that the sender must be the owner of the mail server as designated by the email address info@nodeholder.com
. And yet info@nodeholder.com
is what is specified in both ghost.{development,production}.json
.
We can surmise that SMTPConnection._onData()
is a callback that handles SMTP data and it failed, thus calling createMailError
. But why?
Using the debugger
Subscribe and watch the terminal output of the NodeJS process:

Console output of node process
Error: Can't send mail - all recipients were rejected: 553 5.7.1
<noreply@localhost>: Sender address rejected: not owned by user info@nodeholder.com
at createMailError
(/var/www/ghost/versions/5.47.1/core/server/services/mail/GhostMailer.js:70:12)
at TLSSocket.SMTPConnection._onSocketData
(/var/www/ghost/versions/5.47.1/node_modules/nodemailer/lib/smtp-connection/index.js:193:44)
GhostMailer.js
The error was generated by nodemailer/lib/smtp-connection/index.js
and reported by core/server/services/mail/GhostMailer.js
. Either GhostMailer passed the wrong info to the smtp-connection, or, the smtp-connection is broken.
Searching the GhostMailer.js
file we see where the message is sent, lets re-run and break there:

Note the message being passed: from
is noreply@localhost
. That should be info@nodeholder.com
as specified in config.development.json
. This message doesn't have a chance of working since the SMTP server requires a proper FROM
field and noreply@localhost
is not the valid email address.
sendMagicLink
The call stack shows sendMagicLink
calls sendMail
which calls send
. One of these is the culprit. Lets step into sendMail(message)
.

getAuthEmailFromAddress
We see the from
field is set by config.getAuthEmailFromAddress()
. Let's step in. The config
object is a MembersConfigProvider class which is doing the semantic switch from "get an authentication email address" to "get email support address".

Root cause of bug
It seems we have found the root cause: MembersConfigProvider: getAuthEmailFromAddress()
should be getting an address that is more like "smtpFromAddress" which would translate directly to the semantics used in config.production.js
.

Here's MemoryCache.js
getting the wrong value of noreply
.

Okay, so we've seen that when preparing to communicate with the SMTP server, Ghost calls getAuthEmailFromAddress
but that reaches into a memory cache key/value of members_support_address='noreply'
which is incorrect. Instead, it should be getting the SMTP from
field in config.production.json
which is info@nodeholder.com
.
"mail": {
"from": "'NH Info' <info@nodeholder.com>",
"transport": "SMTP",
"options": {
"host": "smtp.xxxxxxxx.com",
"port": 587,
"auth": {
"user": "info@nodeholder.com",
"pass": "xxxxxxx"
}
}
createMailError confirmation
Lets let it run to createMailError()
and see what the smtp-connection returned:

The SMTP server reports the wrong FROM value
The SMTP server (smtp.somecompany.com) expects the from field to be info@nodeholder.com
but in actuality it is noreply@localhost
. This conflict with what ghost:config reports (`info@nodeholder.com`). This implies the Ghost system is not honoring the configuration information in ghost.{development,production}.json
files.
Solution
Quick and dirty
A workaround is to hardcode the string returned by getAuthEmailFromAddress found in:
/var/www/ghost/versions/5.47.1/core/server/services/members/MembersConfigProvider.js
class MembersConfigProvider {
getAuthEmailFromAddress() {
return "info@nodeholder.com";
// return this.getEmailSudpportAddress();
}
This means the underlying SMTP calls will be satisfied for this particular instance but this is a brittle fix.
For good measure, lets capture the SMTP traffic as described in Ghost Debugging 5: Capturing network packets to confirm our analysis.