@cocalc/hub
Version:
CoCalc: Backend webserver component
809 lines (790 loc) • 29.1 kB
JavaScript
;
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.email_verification_problem = exports.email_verified_successfully = exports.welcome_email = exports.mass_email = exports.send_email = exports.is_banned = exports.send_invite_email = exports.escape_email_body = void 0;
//########################################
// Sending emails
//########################################
const BANNED_DOMAINS = { "qq.com": true };
const util_1 = require("util");
const fs = __importStar(require("fs"));
const os_path = __importStar(require("path"));
const lodash_1 = require("lodash");
const fs_readFile_prom = (0, util_1.promisify)(fs.readFile);
const logger_1 = require("./logger");
const lodash_2 = require("lodash");
const site_defaults_1 = require("@cocalc/util/db-schema/site-defaults");
const base_path_1 = __importDefault(require("@cocalc/backend/base-path"));
const data_1 = require("@cocalc/backend/data");
// sendgrid API: https://sendgrid.com/docs/API_Reference/Web_API/mail.html
const client_1 = __importDefault(require("@sendgrid/client"));
const nodemailer_1 = require("nodemailer");
const misc_1 = require("@cocalc/util/misc");
const site_defaults_2 = require("@cocalc/util/db-schema/site-defaults");
const sanitize_html_1 = __importDefault(require("sanitize-html"));
const misc_2 = require("@cocalc/backend/misc");
const theme_1 = require("@cocalc/util/theme");
const async = __importStar(require("async"));
const winston = (0, logger_1.getLogger)("email");
function escape_email_body(body, allow_urls) {
// in particular, no img and no anchor a
const allowedTags = [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"p",
"ul",
"ol",
"nl",
"li",
"b",
"i",
"strong",
"em",
"strike",
"code",
"hr",
"br",
"div",
"table",
"thead",
"caption",
"tbody",
"tr",
"th",
"td",
"pre",
];
if (allow_urls) {
allowedTags.push("a");
}
return (0, sanitize_html_1.default)(body, { allowedTags });
}
exports.escape_email_body = escape_email_body;
function fallback(val, alt) {
if (typeof val == "string" && val.length > 0) {
return val;
}
else {
return alt;
}
}
// global state
let sendgrid_server = undefined;
let sendgrid_server_disabled = false;
let smtp_server = undefined;
let smtp_server_created = undefined; // timestamp
let smtp_server_conf = undefined;
let smtp_pw_reset_server = undefined;
async function init_sendgrid(opts, dbg) {
if (sendgrid_server != null) {
return;
}
dbg("sendgrid not configured, starting...");
try {
// settings.sendgrid_key takes precedence over a local config file
let api_key = "";
const ssgk = opts.settings.sendgrid_key;
if (typeof ssgk == "string" && ssgk.trim().length > 0) {
dbg("... using site settings/sendgrid_key");
api_key = ssgk.trim();
}
else {
const filename = os_path.join(data_1.secrets, "sendgrid");
try {
api_key = await fs_readFile_prom(filename, "utf8");
api_key = api_key.toString().trim();
dbg(`... using sendgrid_key stored in ${filename}`);
}
catch (err) {
throw new Error(`unable to read the file '${filename}', which is needed to send emails -- ${err}`);
dbg(err);
}
}
if (api_key.length === 0) {
dbg("sendgrid_server: explicitly disabled -- so pretend to always succeed for testing purposes");
sendgrid_server_disabled = true;
}
else {
client_1.default.setApiKey(api_key);
sendgrid_server = client_1.default;
dbg("started sendgrid client");
}
}
catch (err) {
dbg(`Problem initializing Sendgrid -- ${err}`);
}
}
async function init_smtp_server(opts, dbg) {
const s = opts.settings;
const conf = {
host: s.email_smtp_server,
port: s.email_smtp_port,
secure: s.email_smtp_secure,
auth: {
user: s.email_smtp_login,
pass: s.email_smtp_password,
},
};
// we check, if we can keep the smtp server instance
if (smtp_server != null &&
smtp_server_conf != null &&
s._timestamp != null &&
smtp_server_created != null) {
if (smtp_server_created < s._timestamp) {
if (!(0, lodash_1.isEqual)(smtp_server_conf, conf)) {
dbg("SMTP server instance outdated, recreating");
}
else {
// settings changed, but the server config is the same
smtp_server_created = Date.now();
return;
}
}
else {
return;
}
}
dbg("SMTP server not configured. setting up ...");
smtp_server = await (0, nodemailer_1.createTransport)(conf);
smtp_server_created = Date.now();
smtp_server_conf = conf;
dbg("SMTP server configured");
}
async function send_via_smtp(opts, dbg) {
dbg("sending email via SMTP backend");
const msg = {
from: opts.from,
to: opts.to,
subject: opts.subject,
html: smtp_email_body(opts),
};
if (opts.replyto) {
msg.replyTo = opts.replyto;
}
if (opts.cc != null && opts.cc.length > 0) {
msg.cc = opts.cc;
}
if (opts.bcc != null && opts.bcc.length > 0) {
msg.bcc = opts.bcc;
}
const info = await smtp_server.sendMail(msg);
dbg(`sending email via SMTP succeeded -- message id='${info.messageId}'`);
return info.messageId;
}
async function send_via_sendgrid(opts, dbg) {
dbg(`sending email to ${opts.to} starting...`);
// Sendgrid V3 API -- https://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/index.html
// no "to" field, that's in "personalizations!"
const msg = {
from: { email: opts.from, name: opts.fromname },
subject: opts.subject,
content: [
{
type: "text/html",
value: opts.body,
},
],
// plain template with a header (cocalc logo), a h1 title, and a footer
template_id: theme_1.SENDGRID_TEMPLATE_ID,
personalizations: [
{
subject: opts.subject,
to: [
{
email: opts.to,
},
],
},
],
// This #title# will end up below the header in an <h1> according to the template
substitutions: {
"#title#": opts.subject,
},
};
if (opts.replyto) {
msg.reply_to = {
name: opts.replyto_name ?? opts.replyto,
email: opts.replyto,
};
}
if (opts.cc != null && opts.cc.length > 0) {
msg.cc = [{ email: opts.cc }];
}
if (opts.bcc != null && opts.bcc.length > 0) {
msg.bcc = [{ email: opts.bcc }];
}
// one or more strings to categorize the sent emails on sendgrid
if (opts.category != null) {
if (typeof opts.category == "string") {
msg.categories = [opts.category];
}
else if (Array.isArray(opts.category)) {
msg.categories = opts.category;
}
}
// to unsubscribe only from a specific type of email, not everything!
// https://app.sendgrid.com/suppressions/advanced_suppression_manager
if (opts.asm_group != null) {
msg.asm = { group_id: opts.asm_group };
}
dbg(`sending email to ${opts.to} -- data -- ${(0, misc_1.to_json)(msg)}`);
const req = {
body: msg,
method: "POST",
url: "/v3/mail/send",
};
return new Promise((done, fail) => {
sendgrid_server
.request(req)
.then(([_, body]) => {
dbg(`sending email to ${opts.to} -- success -- ${(0, misc_1.to_json)(body)}`);
done();
})
.catch((err) => {
dbg(`sending email to ${opts.to} -- error = ${(0, misc_1.to_json)(err)}`);
fail(err);
});
});
}
// constructs the email body for INVITES! (collaborator and student course)
// this includes sign up instructions pointing to the given project
// it might throw an error!
function create_email_body(subject, body, email_address, project_title, link2proj, allow_urls_in_emails) {
let direct_link;
let base_url;
if (link2proj != null) {
const base_url_segments = link2proj.split("/");
base_url = `${base_url_segments[0]}//${base_url_segments[2]}`;
direct_link = `Open <a href='${link2proj}'>the project '${project_title}'</a>.`;
}
else {
// no link2proj provided -- show something useful:
direct_link = "";
base_url = "https://cocalc.com";
}
let email_body = "";
if (body) {
email_body = escape_email_body(body, allow_urls_in_emails);
// we check if there are plain URLs, which can be used to send SPAM
if (!allow_urls_in_emails && (0, misc_2.contains_url)(email_body)) {
throw new Error("Sorry, links to specific websites are not allowed!");
}
}
else {
email_body = subject;
}
email_body += `
<br/><br/>
<b>To accept the invitation:
<ol>
<li>Open <a href="${base_url}/app">CoCalc</a></li>
<li>Sign up/in using <i>exactly</i> your email address <code>${email_address}</code></li>
<li>${direct_link}</li>
</ol></b>
<br/><br />
(If you're already signed in via <i>another</i> email address,
you have to sign out and sign up/in using the mentioned email address.)
`;
return email_body;
}
function send_invite_email(opts) {
try {
const email_body = create_email_body(opts.subject, opts.email, opts.email_address, opts.title, opts.link2proj, opts.allow_urls);
send_email({
to: opts.to,
bcc: opts.settings.kucalc === site_defaults_1.KUCALC_COCALC_COM ? "invites@cocalc.com" : "",
fromname: fallback(opts.settings.organization_name, theme_1.COMPANY_NAME),
from: fallback(opts.settings.organization_email, theme_1.COMPANY_EMAIL),
category: "invite",
asm_group: theme_1.SENDGRID_ASM_INVITES,
settings: opts.settings,
subject: opts.subject,
body: email_body,
replyto: opts.replyto,
replyto_name: opts.replyto_name,
cb: opts.cb,
});
}
catch (err) {
opts.cb(err);
}
}
exports.send_invite_email = send_invite_email;
function is_banned(address) {
const i = address.indexOf("@");
if (i === -1) {
return false;
}
const x = address.slice(i + 1).toLowerCase();
return !!BANNED_DOMAINS[x];
}
exports.is_banned = is_banned;
function make_dbg(opts) {
if (opts.verbose) {
return (m) => winston.debug(`send_email(to:${opts.to}) -- ${m}`);
}
else {
return function (_) { };
}
}
async function init_pw_reset_smtp_server(opts) {
const s = opts.settings;
if (smtp_pw_reset_server != null) {
return;
}
// s.password_reset_smtp_from;
smtp_pw_reset_server = await (0, nodemailer_1.createTransport)({
host: s.password_reset_smtp_server,
port: s.password_reset_smtp_port,
secure: s.password_reset_smtp_secure,
auth: {
user: s.password_reset_smtp_login,
pass: s.password_reset_smtp_password,
},
});
}
const smtp_footer = `
<p style="margin-top:150px; border-top: 1px solid gray; color: gray; font-size:85%; text-align:center">
This email was sent by <a href="<%= url %>"><%= settings.site_name %></a> by <%= company_name %>.
Contact <a href="mailto:<%= settings.help_email %>"><%= settings.help_email %></a> if you have any questions.
</p>`;
// construct the actual HTML body of a password reset email sent via SMTP
// in particular, all emails must have a body explaining who sent it!
const pw_reset_body_tmpl = (0, lodash_2.template)(`
<h2><%= subject %></h2>
<%= body %>
${smtp_footer}
`);
function password_reset_body(opts) {
return pw_reset_body_tmpl(opts);
}
const smtp_email_body_tmpl = (0, lodash_2.template)(`
<%= body %>
${smtp_footer}
`);
// construct the email body for mails sent via smtp
function smtp_email_body(opts) {
return smtp_email_body_tmpl(opts);
}
const opts_default = {
subject: misc_1.required,
body: misc_1.required,
fromname: undefined,
from: undefined,
to: misc_1.required,
replyto: undefined,
replyto_name: undefined,
cc: "",
bcc: "",
verbose: true,
cb: undefined,
category: undefined,
asm_group: undefined,
settings: misc_1.required,
};
// here's how I test this function:
// require('email').send_email(subject:'TEST MESSAGE', body:'body', to:'wstein@sagemath.com', cb:console.log)
async function send_email(opts) {
const settings = opts.settings;
const company_name = fallback(settings.organization_name, theme_1.COMPANY_NAME);
opts_default.fromname = opts_default.fromname || company_name;
opts_default.from = opts_default.from || settings.organization_email;
opts = (0, misc_1.defaults)(opts, opts_default);
opts.company_name = company_name;
const dns = fallback(settings.dns, theme_1.DNS);
opts.url = `https://${dns}`;
const dbg = make_dbg(opts);
dbg(`${opts.body.slice(0, 201)}...`);
if (is_banned(opts.to) || is_banned(opts.from)) {
dbg("WARNING: attempt to send banned email");
if (typeof opts.cb === "function") {
opts.cb("banned domain");
}
return;
}
// logic:
// 0. email_enabled == false, don't send any emails, period.
// 1. email_backend == none, can't send usual emails
// == sendgrid | smtp → send using one of these
// 2. password_reset_override == 'default', do what (1.) is set to
// == 'smtp', override (1.), including "none"
// an optional message to log and report back
let message = undefined;
if (opts.settings.email_enabled == false) {
const x = site_defaults_2.site_settings_conf.email_enabled.name;
message = `sending any emails is disabled -- see 'Admin/Site Settings/${x}'`;
dbg(message);
}
const pw_reset_smtp = opts.category == "password_reset" &&
opts.settings.password_reset_override == "smtp";
const email_verify_smtp = opts.category == "verify" &&
opts.settings.password_reset_override == "smtp";
const email_backend = opts.settings.email_backend ?? "sendgrid";
try {
// this is a password reset or email verification token email
// and we send it via smtp because the override is enabled
if (pw_reset_smtp || email_verify_smtp) {
dbg("initializing PW SMTP server...");
await init_pw_reset_smtp_server(opts);
const html = opts.category == "verify" ? opts.body : password_reset_body(opts);
dbg(`sending email category=${opts.category} via SMTP server ...`);
const info = await smtp_pw_reset_server.sendMail({
from: opts.settings.password_reset_smtp_from,
replyTo: opts.settings.password_reset_smtp_from,
to: opts.to,
subject: opts.subject,
html,
});
message = `password reset email sent via SMTP: ${info.messageId}`;
dbg(message);
}
else {
// INIT phase
await init_sendgrid(opts, dbg);
await init_smtp_server(opts, dbg);
// SEND phase
switch (email_backend) {
case "sendgrid":
// if not available for any reason …
if (sendgrid_server == null || sendgrid_server_disabled) {
message = "sendgrid email is disabled -- no actual message sent";
dbg(message);
}
else {
await send_via_sendgrid(opts, dbg);
}
break;
case "smtp":
await send_via_smtp(opts, dbg);
break;
case "none":
message =
"no email sent, because email_backend is 'none' -- configure it in 'Admin/Site Settings'";
dbg(message);
break;
}
}
// all fine, no errors
typeof opts.cb === "function" ? opts.cb(undefined, message) : undefined;
}
catch (err) {
if (err) {
// so next time it will try fresh to connect to email server, rather than being wrecked forever.
sendgrid_server = undefined;
err = `error sending email -- ${(0, misc_1.to_json)(err)}`;
dbg(err);
}
else {
dbg("successfully sent email");
}
typeof opts.cb === "function" ? opts.cb(err, message) : undefined;
}
}
exports.send_email = send_email;
// Send a mass email to every address in a file.
// E.g., put the email addresses in a file named 'a' and
// require('email').mass_email(subject:'TEST MESSAGE', body:'body', to:'a', cb:console.log)
function mass_email(opts) {
opts = (0, misc_1.defaults)(opts, {
subject: misc_1.required,
body: misc_1.required,
from: theme_1.COMPANY_EMAIL,
fromname: theme_1.COMPANY_NAME,
to: misc_1.required,
cc: "",
limit: 10,
cb: undefined,
}); // cb(err, list of recipients that we succeeded in sending email to)
const dbg = (m) => winston.debug(`mass_email: ${m}`);
dbg(opts.filename);
dbg(opts.subject);
dbg(opts.body);
const success = [];
const recipients = [];
return async.series([
function (cb) {
if (typeof opts.to !== "string") {
recipients.push(opts.to);
cb();
}
else {
fs.readFile(opts.to, function (err, data) {
if (err) {
cb(err);
}
else {
recipients.push(...(0, misc_1.split)(data.toString()));
cb();
}
});
}
},
function (cb) {
let n = 0;
const f = function (to, cb) {
if (n % 100 === 0) {
dbg(`${n}/${recipients.length - 1}`);
}
n += 1;
send_email({
subject: opts.subject,
body: opts.body,
from: opts.from,
fromname: opts.fromname,
to,
cc: opts.cc,
asm_group: theme_1.SENDGRID_ASM_NEWSLETTER,
category: "newsletter",
verbose: false,
settings: {},
cb(err) {
if (!err) {
success.push(to);
cb();
}
else {
cb(`error sending email to ${to} -- ${err}`);
}
},
});
};
async.mapLimit(recipients, opts.limit, f, cb);
},
], (err) => (typeof opts.cb === "function" ? opts.cb(err, success) : undefined));
}
exports.mass_email = mass_email;
function verify_email_html(token_url) {
return `
<p style="margin-top:0;margin-bottom:20px;">
<strong>
Please <a href="${token_url}">click here</a> to verify your email address!
</strong>
</p>
<p style="margin-top:0;margin-bottom:20px;">
If this link does not work, please copy/paste this URL into a new browser tab and open the link:
</p>
<pre style="margin-top:10px;margin-bottom:10px;font-size:11px;">
${token_url}
</pre>
`;
}
// beware, this needs to be HTML which is compatible with email-clients!
function welcome_email_html({ token_url, verify_emails, site_name, url }) {
return `\
<h1>Welcome to ${site_name}</h1>
<p style="margin-top:0;margin-bottom:10px;">
<a href="${url}">${site_name}</a> helps you to work with open-source scientific software in your web browser.
</p>
<p style="margin-top:0;margin-bottom:20px;">
You received this email because an account with your email address was created.
This was either initiated by you, a friend or colleague invited you, or you're
a student as part of a course.
</p>
${verify_emails ? verify_email_html(token_url) : ""}
<hr size="1"/>
<h3>Exploring ${site_name}</h3>
<p style="margin-top:0;margin-bottom:10px;">
In ${site_name} your work happens inside <strong>private projects</strong>.
These are personal workspaces which contain your files, computational worksheets, and data.
You can run your computations through the web interface, via interactive worksheets and notebooks, or by executing a program in a terminal.
${site_name} supports online editing of
<a href="https://jupyter.org/">Jupyter Notebooks</a>,
<a href="https://www.sagemath.org/">Sage Worksheets</a>,
<a href="https://en.wikibooks.org/wiki/LaTeX">Latex files</a>, etc.
</p>
<p style="margin-top:0;margin-bottom:10px;">
<strong>How to get from 0 to 100:</strong>
</p>
<ul>
<li style="margin-top:0;margin-bottom:10px;">
<strong><a href="https://doc.cocalc.com/">CoCalc Manual:</a></strong> learn more about CoCalc's features.
</li>
<li style="margin-top:0;margin-bottom:10px;">
<a href="https://doc.cocalc.com/jupyter.html">Working with Jupyter Notebooks</a>
</li>
<li style="margin-top:0;margin-bottom:10px;">
<a href="https://doc.cocalc.com/sagews.html">Working with SageMath Worksheets</a>
</li>
<li style="margin-top:0;margin-bottom:10px;">
<strong><a href="https://cocalc.com/policies/pricing.html">Subscriptions:</a></strong> make hosting more robust and increase project quotas
</li>
<li style="margin-top:0;margin-bottom:10px;">
<a href="https://doc.cocalc.com/teaching-instructors.html">Teaching a course on CoCalc</a>.
</li>
<li style="margin-top:0;margin-bottom:10px;">
<a href="https://doc.cocalc.com/howto/connectivity-issues.html">Troubleshooting connectivity issues</a>
</li>
<li style="margin-top:0;margin-bottom:10px;">
<a href="https://github.com/sagemathinc/cocalc/wiki/MathematicalSyntaxErrors">Common mathematical syntax errors:</a> look into this if you are new to working with a programming language!
</li>
</ul>
<p style="margin-top:0;margin-bottom:20px;">
<strong>Collaboration:</strong>
You can invite collaborators to work with you inside a project.
Like you, they can edit the files in that project.
Edits are visible in <strong>real time</strong> for everyone online.
You can share your thoughts in a <strong>side chat</strong> next to each document.
</p>
<p><strong>Software:</strong>
<ul>
<li style="margin-top:0;margin-bottom:10px;">Mathematical calculation:
<a href="https://www.sagemath.org/">SageMath</a>,
<a href="https://www.sympy.org/">SymPy</a>, etc.
</li>
<li style="margin-top:0;margin-bottom:10px;">Statistics and Data Science:
<a href="https://www.r-project.org/">R project</a>,
<a href="http://pandas.pydata.org/">Pandas</a>,
<a href="http://www.statsmodels.org/">statsmodels</a>,
<a href="http://scikit-learn.org/">scikit-learn</a>,
<a href="http://www.nltk.org/">NLTK</a>, etc.
</li>
<li style="margin-top:0;margin-bottom:10px;">Various other computation:
<a href="https://www.tensorflow.org/">Tensorflow</a>,
<a href="https://www.gnu.org/software/octave/">Octave</a>,
<a href="https://julialang.org/">Julia</a>, etc.
</li>
</ul>
<p style="margin-top:0;margin-bottom:20px;">
Visit our <a href="https://cocalc.com/doc/software.html">Software overview page</a> for more details!
</p>
<p style="margin-top:20px;margin-bottom:10px;">
<strong>Questions?</strong>
</p>
<p style="margin-top:0;margin-bottom:10px;">
Schedule a Live Demo with a specialist from CoCalc: <a href="${theme_1.LIVE_DEMO_REQUEST}">request form</a>.
</p>
<p style="margin-top:0;margin-bottom:20px;">
In case of problems, concerns why you received this email, or other questions please contact:
<a href="mailto:${theme_1.HELP_EMAIL}">${theme_1.HELP_EMAIL}</a>.
</p>
\
`;
}
function welcome_email(opts) {
let body, category, subject;
opts = (0, misc_1.defaults)(opts, {
to: misc_1.required,
token: misc_1.required,
only_verify: false,
settings: misc_1.required,
cb: undefined,
});
if (opts.to == null) {
// users can sign up without an email address. ignore this.
typeof opts.cb === "function" ? opts.cb(undefined) : undefined;
return;
}
const settings = opts.settings;
const site_name = fallback(settings.site_name, theme_1.SITE_NAME);
const dns = fallback(settings.dns, theme_1.DNS);
const url = `https://${dns}`;
const token_query = encodeURI(`email=${encodeURIComponent(opts.to)}&token=${opts.token}`);
const endpoint = os_path.join(base_path_1.default, "auth", "verify");
const token_url = `${url}${endpoint}?${token_query}`;
const verify_emails = opts.settings.verify_emails ?? true;
if (opts.only_verify) {
// only send the verification email, if settings.verify_emails is true
if (!verify_emails)
return;
subject = `Verify your email address on ${site_name} (${dns})`;
body = verify_email_html(token_url);
category = "verify";
}
else {
subject = `Welcome to ${site_name} - ${dns}`;
body = welcome_email_html({ token_url, verify_emails, site_name, url });
category = "welcome";
}
send_email({
subject,
body,
fromname: fallback(settings.organization_name, theme_1.COMPANY_NAME),
from: fallback(settings.organization_email, theme_1.COMPANY_EMAIL),
to: opts.to,
cb: opts.cb,
category,
settings: opts.settings,
asm_group: 147985,
}); // https://app.sendgrid.com/suppressions/advanced_suppression_manager
}
exports.welcome_email = welcome_email;
function email_verified_successfully(url) {
const title = `${theme_1.SITE_NAME}: Email verification successful`;
return `<DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="5;url=${url}" />
<style>
* {font-family: sans-serif;}
</style>
<title>${title}</title>
</head>
<body>
<h1>Email verification successful!</h1>
<div>
Click <a href="${url}">here</a> if you aren't automatically redirected to <a href="${url}">${theme_1.SITE_NAME}</a> within 30 seconds.
</div>
</body>
</html>
`;
}
exports.email_verified_successfully = email_verified_successfully;
function email_verification_problem(url, problem) {
const title = `${theme_1.SITE_NAME}: Email verification problem`;
return `<DOCTYPE html>
<html>
<head>
<style>
div, p, h1, h2 {font-family: sans-serif;}
div {margin-top: 1rem;}
</style>
<title>${title}</title>
</head>
<body>
<h1>${title}</h1>
<div>There was a problem verifying your email address.</div>
<div>Reason: <code>${problem}</code></div>
<div>
Continue to <a href="${url}">${theme_1.SITE_NAME}</a> or
contact support: <a href="mailto:${theme_1.HELP_EMAIL}">${theme_1.HELP_EMAIL}</a>.
</div>
</body>
</html>
`;
}
exports.email_verification_problem = email_verification_problem;
//# sourceMappingURL=email.js.map