Files
Sravan 7bc40d8397 feat(meetings): email invites + add external participants by email + share link (batch69)
#4: scheduled meetings can now invite people who aren't on Connect.
- SMTP config (config.js, env-gated: SMTP_HOST/PORT/USER/PASS/FROM/SECURE,
  PUBLIC_BASE_URL) + a small nodemailer wrapper (mailer.js) with a branded
  meeting-invite template carrying the guest join link. No-op until SMTP is set.
- /api/meetings/schedule + /update accept participantEmails; external emails are
  persisted (scheduled_meetings.guest_emails migration) and emailed the guest
  link (plus any invited Connect users with an email on file). Fire-and-forget —
  a mail outage never fails scheduling.
- Meetings list DTO returns `link`; schedule form gains an "Invite by email"
  chip input; each scheduled-meeting card gets a "Copy link" action.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:02:45 +05:30

83 lines
4.0 KiB
JavaScript

// Outbound email (meeting invites to external participants — #4). Config-gated: does nothing unless
// SMTP_* env vars are set (SMTP_ENABLED). Credentials come from config (env), never the client.
//
// Kept intentionally small: one lazily-created nodemailer transport + a couple of helpers. Sending is
// fire-and-forget from the caller's perspective (we log failures but never throw into request handlers,
// so a mail outage can't break scheduling).
const cfg = require('./config');
let _transport = null;
let _nodemailer = null;
function transport() {
if (!cfg.SMTP_ENABLED) return null;
if (_transport) return _transport;
try {
_nodemailer = _nodemailer || require('nodemailer');
_transport = _nodemailer.createTransport({
host: cfg.SMTP_HOST,
port: cfg.SMTP_PORT,
secure: cfg.SMTP_SECURE, // true for 465, false for 587/STARTTLS
auth: { user: cfg.SMTP_USER, pass: cfg.SMTP_PASS },
});
} catch (e) {
console.warn('[mailer] transport init failed:', e && e.message);
_transport = null;
}
return _transport;
}
const isEnabled = () => cfg.SMTP_ENABLED;
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
}
// Send one email. Returns a promise that resolves to true/false — never rejects (callers shouldn't
// have to try/catch around scheduling). `to` may be a string or an array of addresses.
function send({ to, subject, html, text }) {
return new Promise((resolve) => {
const t = transport();
if (!t) { console.warn('[mailer] SMTP not configured — skipping email:', subject); return resolve(false); }
const list = Array.isArray(to) ? to.filter(Boolean) : [to].filter(Boolean);
if (!list.length) return resolve(false);
t.sendMail({ from: cfg.SMTP_FROM, to: list.join(', '), subject, text: text || '', html: html || undefined }, (err) => {
if (err) { console.warn('[mailer] sendMail failed:', err && err.message); return resolve(false); }
resolve(true);
});
});
}
// Branded meeting-invite email. `link` is the guest join URL; `when` a human-readable time string.
function meetingInviteEmail({ title, when, link, host, description }) {
const subject = 'Meeting invite: ' + title;
const text = [
(host ? host + ' invited you to a meeting.' : 'You have a meeting invite.'),
'',
'Title: ' + title,
when ? ('When: ' + when) : '',
description ? ('Details: ' + description) : '',
'',
'Join: ' + link,
'',
'No account needed — just open the link and enter your name.',
].filter(Boolean).join('\n');
const html = `<div style="font-family:'Segoe UI',system-ui,sans-serif;max-width:520px;margin:0 auto;color:#0f172a">
<div style="background:#1F3B73;border-radius:14px 14px 0 0;padding:20px 24px;color:#fff">
<div style="font-size:18px;font-weight:700">Biz Connect</div>
<div style="opacity:.85;font-size:13px;margin-top:2px">Meeting invitation</div>
</div>
<div style="border:1px solid #e3e8f2;border-top:none;border-radius:0 0 14px 14px;padding:22px 24px">
<p style="margin:0 0 14px;font-size:14px">${host ? esc(host) + ' invited you to a meeting.' : 'You have a meeting invite.'}</p>
<div style="font-size:17px;font-weight:700;margin-bottom:6px">${esc(title)}</div>
${when ? `<div style="font-size:14px;color:#475569;margin-bottom:4px">🗓 ${esc(when)}</div>` : ''}
${description ? `<div style="font-size:13px;color:#64748b;margin:10px 0 0;line-height:1.5">${esc(description)}</div>` : ''}
<a href="${esc(link)}" style="display:inline-block;margin:18px 0 8px;background:#1F3B73;color:#fff;text-decoration:none;font-weight:600;font-size:14px;padding:11px 22px;border-radius:9px">Join the meeting</a>
<div style="font-size:12px;color:#94a3b8;margin-top:8px">No account needed — open the link and enter your name.</div>
<div style="font-size:11px;color:#cbd5e1;margin-top:14px;word-break:break-all">${esc(link)}</div>
</div>
</div>`;
return { subject, text, html };
}
module.exports = { send, meetingInviteEmail, isEnabled };