If you add your email, we will send you a short confirmation message right away, and someone from the team will reach out to you personally. It is a one-time email, and we will not add you to any mailing list. If you leave it blank, we will still receive your message, but we will not be able to reply.
@@ -756,8 +775,14 @@
-
Pardoned. Welcome to the family of God.
-
“Therefore, there is now no condemnation for those who are in Christ Jesus.” (Romans 8:1) You are no longer a defendant. You are a child of the Judge. Thank you for letting us know. We will be glad to hear from you again, and Start Here is a good path through the podcast and studies, or you can ask a question.
+
✓
+
Your message has been sent.
+
+
+
Pardoned. Welcome to the family of God.
+
“Therefore, there is now no condemnation for those who are in Christ Jesus.” (Romans 8:1) You are no longer a defendant. You are a child of the Judge.
+
While you wait, begin with the Gospel of John, talk to God every day, and look for a Bible-teaching church. Start Here is a good path through the podcast and studies, and you can ask a question any time.
+
@@ -1621,8 +1646,23 @@ buildTimeline();
});
const data = await res.json().catch(function () { return {}; });
if (!res.ok) throw new Error(data.message || 'We could not send this right now.');
+ const reach = document.getElementById('sent-reach');
+ const given = (form.email.value || '').trim();
+ if (given) {
+ reach.textContent = '';
+ reach.appendChild(document.createTextNode(data.confirmationSent ? 'We just sent a confirmation email to ' : 'Someone from the team will reach out to you soon at '));
+ const strong = document.createElement('strong');
+ strong.textContent = given;
+ reach.appendChild(strong);
+ reach.appendChild(document.createTextNode(data.confirmationSent
+ ? ', and someone from the team will reach out to you personally soon. If you do not see our emails, please check your spam folder.'
+ : '. If you do not see a reply in a few days, please check your spam folder.'));
+ } else {
+ reach.textContent = 'We are rejoicing with you. Because you did not leave an email address, we are not able to reach out to you, but you are always welcome to write to us.';
+ }
intro.style.display = 'none';
done.classList.add('open');
+ done.scrollIntoView({ block: 'center' });
done.focus();
} catch (ex) {
err.textContent = (ex && ex.message ? ex.message : 'We could not send this right now.') + ' Your decision is between you and God either way, and you are welcome to write to us from the contact page.';
diff --git a/server/routes/salvation.js b/server/routes/salvation.js
index a029aec..78371f2 100644
--- a/server/routes/salvation.js
+++ b/server/routes/salvation.js
@@ -1,10 +1,25 @@
import { Resend } from 'resend'
import { escapeHtml } from '../helpers.js'
-import { getResendFromAddress, getResendInboxAddress, sendResendEmailWithRetry } from '../email.js'
+import { getResendFromAddress, getResendInboxAddress, getResendReplyToAddress, getCanonicalBaseUrl, sendResendEmailWithRetry } from '../email.js'
// A visitor who finished the /salvation page can tell us they prayed to receive Christ.
// Nothing is stored on the server: the response is emailed once to the site inbox and that is all.
const hits = new Map()
+// One confirmation per address per hour, so the form can't be used to mail someone repeatedly.
+const confirmedAt = new Map()
+const CONFIRM_COOLDOWN_MS = 60 * 60 * 1000
+
+function mayConfirm(address) {
+ const key = address.toLowerCase()
+ const now = Date.now()
+ const last = confirmedAt.get(key)
+ if (typeof last === 'number' && now - last < CONFIRM_COOLDOWN_MS) return false
+ confirmedAt.set(key, now)
+ if (confirmedAt.size > 2000) {
+ for (const [k, v] of confirmedAt) { if (now - v > CONFIRM_COOLDOWN_MS) confirmedAt.delete(k) }
+ }
+ return true
+}
function rateLimit(req, res, next) {
const ip = req.ip ?? 'unknown'
@@ -28,6 +43,188 @@ function clean(value, max) {
return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ').slice(0, max) : ''
}
+
+// The notification the site owner receives. Styled to match the other admin emails
+// (dark header, gold accent, cream cards) and written to be acted on, not just read.
+function buildSalvationEmail({ name, email, message, when }) {
+ const who = name || 'Someone'
+ const subject = name ? `A new decision for Christ: ${name}` : 'A new decision for Christ on your salvation page'
+ const nameLine = name || 'Name not given'
+ const safeName = escapeHtml(nameLine)
+ const safeEmail = escapeHtml(email)
+ const safeWhen = escapeHtml(when)
+
+ const followUp = email
+ ? [
+ 'Reply to this email within a day or two. A short, warm note is plenty.',
+ 'Congratulate them, and ask them to tell you their story.',
+ 'Point them to the Gospel of John, a Bible-teaching church, and baptism.',
+ 'Invite them to Start Here at versebyversewithnate.us/start-here.',
+ 'Pray for them by name.',
+ ]
+ : ['They did not leave an email address, so there is no way to reply.', 'Pray for them. God knows exactly who they are.']
+
+ const text = [
+ `A VERDICT HAS COME IN`,
+ ``,
+ `${who} pleaded guilty and accepted Christ's pardon on the "The Gospel, in a Courtroom" page and says they prayed to receive Jesus as their Savior.`,
+ ``,
+ `"There is joy in the presence of the angels of God over one sinner who repents." (Luke 15:10)`,
+ ``,
+ `DETAILS`,
+ `Name: ${nameLine}`,
+ `Email: ${email || 'Not given'}`,
+ `When: ${when}`,
+ ``,
+ `THEIR MESSAGE`,
+ message || '(They did not leave a message.)',
+ ``,
+ `SUGGESTED FOLLOW-UP`,
+ ...followUp.map((t, i) => `${i + 1}. ${t}`),
+ ``,
+ `This email is the only record of this response. Nothing is stored on the website, so keep it somewhere safe.`,
+ email ? `Reply directly to this email to reach ${who}.` : '',
+ ].filter(l => l !== '').join('\n')
+
+ const row = (label, value, last = false) =>
+ `
Keep this email. It is the only record of this response. Nothing is stored on the website. ${email ? 'Replying to this message goes straight to them.' : ''}
+
+
+
`
+
+ return { subject, text, html }
+}
+
+// The warm, one-time confirmation the visitor receives at the address they gave.
+function buildSalvationConfirmationEmail({ name }) {
+ const base = getCanonicalBaseUrl().replace(/\/$/, '')
+ const greeting = name ? `Hi ${name},` : 'Hi friend,'
+ const subject = 'We received your message. Welcome to the family of God'
+ const steps = [
+ ['Read the Gospel of John.', 'Start there, then keep going through the New Testament.'],
+ ['Talk to God every day.', 'Prayer is simply talking with him, in your own words.'],
+ ['Tell someone.', 'Share your decision with a Christian friend or family member.'],
+ ['Find a church.', 'Join a Bible-teaching church, and be baptized as a public sign of your faith (Acts 2:38).'],
+ ]
+
+ const text = [
+ greeting,
+ '',
+ 'Thank you for letting us know that you prayed to receive Jesus. We are rejoicing with you, and your message has been received.',
+ 'Someone from our team will reach out to you personally soon.',
+ '',
+ '"Therefore, there is now no condemnation for those who are in Christ Jesus." (Romans 8:1)',
+ 'You are no longer a defendant. You are a child of the Judge.',
+ '',
+ 'WHAT TO DO NEXT',
+ ...steps.map(([h, b], i) => `${i + 1}. ${h} ${b}`),
+ '',
+ `A good path through the podcast and studies: ${base}/start-here`,
+ `Questions? Ask here: ${base}/contact?source=salvation`,
+ '',
+ 'Grace and peace,',
+ 'Verse by Verse with Nate',
+ '',
+ `You are receiving this one-time email because this address was entered on ${base}/salvation. If that was not you, you can safely ignore it.`,
+ ].join('\n')
+
+ const stepRows = steps
+ .map(([h, b], i) =>
+ `
${i + 1}
` +
+ `
${escapeHtml(h)} ${escapeHtml(b)}
`)
+ .join('')
+
+ const html = `
+
+
+
+
Verse by Verse with Nate
+
✓
+
Your message was received
+
Welcome to the family of God.
+
+
+
${escapeHtml(greeting)}
+
Thank you for letting us know that you prayed to receive Jesus. We are rejoicing with you, and your message has been received.
+
Someone from our team will reach out to you personally soon.
+
+
+
+
“Therefore, there is now no condemnation for those who are in Christ Jesus.” Romans 8:1
+
You are no longer a defendant. You are a child of the Judge.
+ Have a question? Ask us here, or just reply to this email.
+
+
+
Grace and peace, Verse by Verse with Nate
+
+
+
You are receiving this one-time email because this address was entered at ${escapeHtml(base)}/salvation. If that was not you, you can safely ignore it.
+
+
+
`
+
+ return { subject, text, html }
+}
+
export function register(app) {
app.post('/api/salvation-response', rateLimit, async (req, res) => {
const { name, email, message, _honey } = req.body ?? {}
@@ -47,26 +244,8 @@ export function register(app) {
return
}
- const when = new Date().toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })
- const nameLine = safeName || 'Name not given'
- const emailLine = safeEmail || 'Email not given'
- const text = [
- 'Someone responded to the gospel on the /salvation page (the courtroom presentation) and says they pleaded guilty, accepted the pardon, and prayed to receive Jesus as Savior.',
- '',
- `Name: ${nameLine}`,
- `Email: ${emailLine}`,
- `When: ${when}`,
- '',
- safeMessage ? `Their message:\n${safeMessage}` : 'They did not leave a message.',
- '',
- safeEmail ? 'You can reply directly to this email to reach them.' : 'They did not leave an email address, so there is no way to reply.',
- ].join('\n')
- const html = `
-
Someone responded to the gospel on the /salvation page and says they prayed to receive Jesus as Savior.