UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

757 lines 38.1 kB
"use strict"; /* * 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; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from) { for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]; return to; }; 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 //######################################## var BANNED_DOMAINS = { "qq.com": true }; var util_1 = require("util"); var fs = __importStar(require("fs")); var os_path = __importStar(require("path")); var lodash_1 = require("lodash"); var fs_readFile_prom = util_1.promisify(fs.readFile); var async = require("async"); var winston = require("./logger").getLogger("email"); var lodash_2 = require("lodash"); var site_defaults_1 = require("smc-util/db-schema/site-defaults"); var base_path_1 = __importDefault(require("smc-util-node/base-path")); var data_1 = require("smc-util-node/data"); // sendgrid API v3: https://sendgrid.com/docs/API_Reference/Web_API/mail.html var sendgrid = __importStar(require("@sendgrid/client")); var nodemailer = __importStar(require("nodemailer")); var misc = require("smc-util/misc"); var defaults = misc.defaults, required = misc.required; var site_defaults_2 = require("smc-util/db-schema/site-defaults"); var sanitize_html_1 = __importDefault(require("sanitize-html")); var misc_1 = require("smc-util-node/misc"); var _a = require("smc-util/theme"), SENDGRID_TEMPLATE_ID = _a.SENDGRID_TEMPLATE_ID, SENDGRID_ASM_NEWSLETTER = _a.SENDGRID_ASM_NEWSLETTER, SENDGRID_ASM_INVITES = _a.SENDGRID_ASM_INVITES, COMPANY_NAME = _a.COMPANY_NAME, COMPANY_EMAIL = _a.COMPANY_EMAIL, SITE_NAME = _a.SITE_NAME, DNS = _a.DNS, HELP_EMAIL = _a.HELP_EMAIL, LIVE_DEMO_REQUEST = _a.LIVE_DEMO_REQUEST; function escape_email_body(body, allow_urls) { // in particular, no img and no anchor a var 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 sanitize_html_1.default(body, { allowedTags: 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 var sendgrid_server = undefined; var sendgrid_server_disabled = false; var smtp_server = undefined; var smtp_server_created = undefined; // timestamp var smtp_server_conf = undefined; var smtp_pw_reset_server = undefined; function init_sendgrid(opts, dbg) { return __awaiter(this, void 0, void 0, function () { var api_key, ssgk, filename, err_1, err_2; return __generator(this, function (_a) { switch (_a.label) { case 0: if (sendgrid_server != null) { return [2 /*return*/]; } dbg("sendgrid not configured, starting..."); _a.label = 1; case 1: _a.trys.push([1, 7, , 8]); api_key = ""; ssgk = opts.settings.sendgrid_key; if (!(typeof ssgk == "string" && ssgk.trim().length > 0)) return [3 /*break*/, 2]; dbg("... using site settings/sendgrid_key"); api_key = ssgk.trim(); return [3 /*break*/, 6]; case 2: filename = os_path.join(data_1.secrets, "sendgrid"); _a.label = 3; case 3: _a.trys.push([3, 5, , 6]); return [4 /*yield*/, fs_readFile_prom(filename, "utf8")]; case 4: api_key = _a.sent(); api_key = api_key.toString().trim(); dbg("... using sendgrid_key stored in " + filename); return [3 /*break*/, 6]; case 5: err_1 = _a.sent(); throw new Error("unable to read the file '" + filename + "', which is needed to send emails -- " + err_1); case 6: if (api_key.length === 0) { dbg("sendgrid_server: explicitly disabled -- so pretend to always succeed for testing purposes"); sendgrid_server_disabled = true; } else { sendgrid.setApiKey(api_key); sendgrid_server = sendgrid; dbg("started sendgrid client"); } return [3 /*break*/, 8]; case 7: err_2 = _a.sent(); dbg("Problem initializing Sendgrid -- " + err_2); return [3 /*break*/, 8]; case 8: return [2 /*return*/]; } }); }); } function init_smtp_server(opts, dbg) { return __awaiter(this, void 0, void 0, function () { var s, conf; return __generator(this, function (_a) { switch (_a.label) { case 0: s = opts.settings; 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 (!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 [2 /*return*/]; } } else { return [2 /*return*/]; } } dbg("SMTP server not configured. setting up ..."); return [4 /*yield*/, nodemailer.createTransport(conf)]; case 1: smtp_server = _a.sent(); smtp_server_created = Date.now(); smtp_server_conf = conf; dbg("SMTP server configured"); return [2 /*return*/]; } }); }); } function send_via_smtp(opts, dbg) { return __awaiter(this, void 0, void 0, function () { var msg, info; return __generator(this, function (_a) { switch (_a.label) { case 0: dbg("sending email via SMTP backend"); 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; } return [4 /*yield*/, smtp_server.sendMail(msg)]; case 1: info = _a.sent(); dbg("sending email via SMTP succeeded -- message id='" + info.messageId + "'"); return [2 /*return*/, info.messageId]; } }); }); } function send_via_sendgrid(opts, dbg) { var _a; return __awaiter(this, void 0, void 0, function () { var msg, req; return __generator(this, function (_b) { dbg("sending email to " + opts.to + " starting..."); 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: 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: (_a = opts.replyto_name) !== null && _a !== void 0 ? _a : 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 -- " + misc.to_json(msg)); req = { body: msg, method: "POST", url: "/v3/mail/send", }; return [2 /*return*/, new Promise(function (done, fail) { sendgrid_server .request(req) .then(function (_a) { var _b = __read(_a, 2), _ = _b[0], body = _b[1]; dbg("sending email to " + opts.to + " -- success -- " + misc.to_json(body)); done(); }) .catch(function (err) { dbg("sending email to " + opts.to + " -- error = " + misc.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) { var direct_link; var base_url; if (link2proj != null) { var 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"; } var 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 && misc_1.contains_url(email_body)) { throw new Error("Sorry, links to specific websites are not allowed!"); } } else { email_body = subject; } email_body += "\n<br/><br/>\n<b>To accept the invitation:\n<ol>\n<li>Open <a href=\"" + base_url + "/app\">CoCalc</a></li>\n<li>Sign up/in using <i>exactly</i> your email address <code>" + email_address + "</code></li>\n<li>" + direct_link + "</li>\n</ol></b>\n<br/><br />\n(If you're already signed in via <i>another</i> email address,\n you have to sign out and sign up/in using the mentioned email address.)\n"; return email_body; } function send_invite_email(opts) { try { var 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, COMPANY_NAME), from: fallback(opts.settings.organization_email, COMPANY_EMAIL), category: "invite", asm_group: 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) { var i = address.indexOf("@"); if (i === -1) { return false; } var x = address.slice(i + 1).toLowerCase(); return !!BANNED_DOMAINS[x]; } exports.is_banned = is_banned; function make_dbg(opts) { if (opts.verbose) { return function (m) { return winston.debug("send_email(to:" + opts.to + ") -- " + m); }; } else { return function (_) { }; } } function init_pw_reset_smtp_server(opts) { return __awaiter(this, void 0, void 0, function () { var s; return __generator(this, function (_a) { switch (_a.label) { case 0: s = opts.settings; if (smtp_pw_reset_server != null) { return [2 /*return*/]; } return [4 /*yield*/, nodemailer.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, }, })]; case 1: // s.password_reset_smtp_from; smtp_pw_reset_server = _a.sent(); return [2 /*return*/]; } }); }); } var smtp_footer = "\n<p style=\"margin-top:150px; border-top: 1px solid gray; color: gray; font-size:85%; text-align:center\">\nThis email was sent by <a href=\"<%= url %>\"><%= settings.site_name %></a> by <%= company_name %>.\nContact <a href=\"mailto:<%= settings.help_email %>\"><%= settings.help_email %></a> if you have any questions.\n</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! var pw_reset_body_tmpl = lodash_2.template("\n<h2><%= subject %></h2>\n\n<%= body %>\n\n" + smtp_footer + "\n"); function password_reset_body(opts) { return pw_reset_body_tmpl(opts); } var smtp_email_body_tmpl = lodash_2.template("\n<%= body %>\n\n" + smtp_footer + "\n"); // construct the email body for mails sent via smtp function smtp_email_body(opts) { return smtp_email_body_tmpl(opts); } var opts_default = { subject: required, body: required, fromname: undefined, from: undefined, to: required, replyto: undefined, replyto_name: undefined, cc: "", bcc: "", verbose: true, cb: undefined, category: undefined, asm_group: undefined, settings: required, }; // here's how I test this function: // require('email').send_email(subject:'TEST MESSAGE', body:'body', to:'wstein@sagemath.com', cb:console.log) function send_email(opts) { var _a; return __awaiter(this, void 0, void 0, function () { var settings, company_name, dns, dbg, message, x, pw_reset_smtp, email_verify_smtp, email_backend, html, info, _b, err_3; return __generator(this, function (_c) { switch (_c.label) { case 0: settings = opts.settings; company_name = fallback(settings.organization_name, COMPANY_NAME); opts_default.fromname = opts_default.fromname || company_name; opts_default.from = opts_default.from || settings.organization_email; opts = defaults(opts, opts_default); opts.company_name = company_name; dns = fallback(settings.dns, DNS); opts.url = "https://" + dns; 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 [2 /*return*/]; } message = undefined; if (opts.settings.email_enabled == false) { x = site_defaults_2.site_settings_conf.email_enabled.name; message = "sending any emails is disabled -- see 'Admin/Site Settings/" + x + "'"; dbg(message); } pw_reset_smtp = opts.category == "password_reset" && opts.settings.password_reset_override == "smtp"; email_verify_smtp = opts.category == "verify" && opts.settings.password_reset_override == "smtp"; email_backend = (_a = opts.settings.email_backend) !== null && _a !== void 0 ? _a : "sendgrid"; _c.label = 1; case 1: _c.trys.push([1, 15, , 16]); if (!(pw_reset_smtp || email_verify_smtp)) return [3 /*break*/, 4]; dbg("initializing PW SMTP server..."); return [4 /*yield*/, init_pw_reset_smtp_server(opts)]; case 2: _c.sent(); html = opts.category == "verify" ? opts.body : password_reset_body(opts); dbg("sending email category=" + opts.category + " via SMTP server ..."); return [4 /*yield*/, 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: html, })]; case 3: info = _c.sent(); message = "password reset email sent via SMTP: " + info.messageId; dbg(message); return [3 /*break*/, 14]; case 4: // INIT phase return [4 /*yield*/, init_sendgrid(opts, dbg)]; case 5: // INIT phase _c.sent(); return [4 /*yield*/, init_smtp_server(opts, dbg)]; case 6: _c.sent(); _b = email_backend; switch (_b) { case "sendgrid": return [3 /*break*/, 7]; case "smtp": return [3 /*break*/, 11]; case "none": return [3 /*break*/, 13]; } return [3 /*break*/, 14]; case 7: if (!(sendgrid_server == null || sendgrid_server_disabled)) return [3 /*break*/, 8]; message = "sendgrid email is disabled -- no actual message sent"; dbg(message); return [3 /*break*/, 10]; case 8: return [4 /*yield*/, send_via_sendgrid(opts, dbg)]; case 9: _c.sent(); _c.label = 10; case 10: return [3 /*break*/, 14]; case 11: return [4 /*yield*/, send_via_smtp(opts, dbg)]; case 12: _c.sent(); return [3 /*break*/, 14]; case 13: message = "no email sent, because email_backend is 'none' -- configure it in 'Admin/Site Settings'"; dbg(message); return [3 /*break*/, 14]; case 14: // all fine, no errors typeof opts.cb === "function" ? opts.cb(undefined, message) : undefined; return [3 /*break*/, 16]; case 15: err_3 = _c.sent(); if (err_3) { // so next time it will try fresh to connect to email server, rather than being wrecked forever. sendgrid_server = undefined; err_3 = "error sending email -- " + misc.to_json(err_3); dbg(err_3); } else { dbg("successfully sent email"); } typeof opts.cb === "function" ? opts.cb(err_3, message) : undefined; return [3 /*break*/, 16]; case 16: return [2 /*return*/]; } }); }); } 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 = defaults(opts, { subject: required, body: required, from: COMPANY_EMAIL, fromname: COMPANY_NAME, to: required, cc: "", limit: 10, cb: undefined, }); // cb(err, list of recipients that we succeeded in sending email to) var dbg = function (m) { return winston.debug("mass_email: " + m); }; dbg(opts.filename); dbg(opts.subject); dbg(opts.body); var success = []; var 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.apply(recipients, __spreadArray([], __read(misc.split(data.toString())))); cb(); } }); } }, function (cb) { var n = 0; var 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: to, cc: opts.cc, asm_group: SENDGRID_ASM_NEWSLETTER, category: "newsletter", verbose: false, settings: {}, cb: function (err) { if (!err) { success.push(to); cb(); } else { cb("error sending email to " + to + " -- " + err); } }, }); }; async.mapLimit(recipients, opts.limit, f, cb); }, ], function (err) { return (typeof opts.cb === "function" ? opts.cb(err, success) : undefined); }); } exports.mass_email = mass_email; function verify_email_html(token_url) { return "\n<p style=\"margin-top:0;margin-bottom:20px;\">\n<strong>\nPlease <a href=\"" + token_url + "\">click here</a> to verify your email address!\n</strong>\n</p>\n\n<p style=\"margin-top:0;margin-bottom:20px;\">\nIf this link does not work, please copy/paste this URL into a new browser tab and open the link:\n</p>\n\n<pre style=\"margin-top:10px;margin-bottom:10px;font-size:11px;\">\n" + token_url + "\n</pre>\n"; } // beware, this needs to be HTML which is compatible with email-clients! function welcome_email_html(_a) { var token_url = _a.token_url, verify_emails = _a.verify_emails, site_name = _a.site_name, url = _a.url; return "<h1>Welcome to " + site_name + "</h1>\n\n<p style=\"margin-top:0;margin-bottom:10px;\">\n<a href=\"" + url + "\">" + site_name + "</a> helps you to work with open-source scientific software in your web browser.\n</p>\n\n<p style=\"margin-top:0;margin-bottom:20px;\">\nYou received this email because an account with your email address was created.\nThis was either initiated by you, a friend or colleague invited you, or you're\na student as part of a course.\n</p>\n\n" + (verify_emails ? verify_email_html(token_url) : "") + "\n\n<hr size=\"1\"/>\n\n<h3>Exploring " + site_name + "</h3>\n<p style=\"margin-top:0;margin-bottom:10px;\">\nIn " + site_name + " your work happens inside <strong>private projects</strong>.\nThese are personal workspaces which contain your files, computational worksheets, and data.\nYou can run your computations through the web interface, via interactive worksheets and notebooks, or by executing a program in a terminal.\n" + site_name + " supports online editing of\n <a href=\"https://jupyter.org/\">Jupyter Notebooks</a>,\n <a href=\"https://www.sagemath.org/\">Sage Worksheets</a>,\n <a href=\"https://en.wikibooks.org/wiki/LaTeX\">Latex files</a>, etc.\n</p>\n\n<p style=\"margin-top:0;margin-bottom:10px;\">\n<strong>How to get from 0 to 100:</strong>\n</p>\n\n<ul>\n<li style=\"margin-top:0;margin-bottom:10px;\">\n <strong><a href=\"https://doc.cocalc.com/\">CoCalc Manual:</a></strong> learn more about CoCalc's features.\n</li>\n<li style=\"margin-top:0;margin-bottom:10px;\">\n <a href=\"https://doc.cocalc.com/jupyter.html\">Working with Jupyter Notebooks</a>\n</li>\n<li style=\"margin-top:0;margin-bottom:10px;\">\n <a href=\"https://doc.cocalc.com/sagews.html\">Working with SageMath Worksheets</a>\n</li>\n<li style=\"margin-top:0;margin-bottom:10px;\">\n <strong><a href=\"https://cocalc.com/policies/pricing.html\">Subscriptions:</a></strong> make hosting more robust and increase project quotas\n</li>\n<li style=\"margin-top:0;margin-bottom:10px;\">\n <a href=\"https://doc.cocalc.com/teaching-instructors.html\">Teaching a course on CoCalc</a>.\n</li>\n<li style=\"margin-top:0;margin-bottom:10px;\">\n <a href=\"https://doc.cocalc.com/howto/connectivity-issues.html\">Troubleshooting connectivity issues</a>\n</li>\n<li style=\"margin-top:0;margin-bottom:10px;\">\n <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!\n</li>\n</ul>\n\n\n<p style=\"margin-top:0;margin-bottom:20px;\">\n<strong>Collaboration:</strong>\nYou can invite collaborators to work with you inside a project.\nLike you, they can edit the files in that project.\nEdits are visible in <strong>real time</strong> for everyone online.\nYou can share your thoughts in a <strong>side chat</strong> next to each document.\n</p>\n\n\n<p><strong>Software:</strong>\n<ul>\n<li style=\"margin-top:0;margin-bottom:10px;\">Mathematical calculation:\n <a href=\"https://www.sagemath.org/\">SageMath</a>,\n <a href=\"https://www.sympy.org/\">SymPy</a>, etc.\n</li>\n<li style=\"margin-top:0;margin-bottom:10px;\">Statistics and Data Science:\n <a href=\"https://www.r-project.org/\">R project</a>,\n <a href=\"http://pandas.pydata.org/\">Pandas</a>,\n <a href=\"http://www.statsmodels.org/\">statsmodels</a>,\n <a href=\"http://scikit-learn.org/\">scikit-learn</a>,\n <a href=\"http://www.nltk.org/\">NLTK</a>, etc.\n</li>\n<li style=\"margin-top:0;margin-bottom:10px;\">Various other computation:\n <a href=\"https://www.tensorflow.org/\">Tensorflow</a>,\n <a href=\"https://www.gnu.org/software/octave/\">Octave</a>,\n <a href=\"https://julialang.org/\">Julia</a>, etc.\n</li>\n</ul>\n\n<p style=\"margin-top:0;margin-bottom:20px;\">\nVisit our <a href=\"https://cocalc.com/doc/software.html\">Software overview page</a> for more details!\n</p>\n\n\n<p style=\"margin-top:20px;margin-bottom:10px;\">\n<strong>Questions?</strong>\n</p>\n<p style=\"margin-top:0;margin-bottom:10px;\">\nSchedule a Live Demo with a specialist from CoCalc: <a href=\"" + LIVE_DEMO_REQUEST + "\">request form</a>.\n</p>\n<p style=\"margin-top:0;margin-bottom:20px;\">\nIn case of problems, concerns why you received this email, or other questions please contact:\n<a href=\"mailto:" + HELP_EMAIL + "\">" + HELP_EMAIL + "</a>.\n</p>\n"; } function welcome_email(opts) { var _a; var body, category, subject; opts = defaults(opts, { to: required, token: required, only_verify: false, settings: 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; } var settings = opts.settings; var site_name = fallback(settings.site_name, SITE_NAME); var dns = fallback(settings.dns, DNS); var url = "https://" + dns; var token_query = encodeURI("email=" + encodeURIComponent(opts.to) + "&token=" + opts.token); var endpoint = os_path.join(base_path_1.default, "auth", "verify"); var token_url = "" + url + endpoint + "?" + token_query; var verify_emails = (_a = opts.settings.verify_emails) !== null && _a !== void 0 ? _a : 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: token_url, verify_emails: verify_emails, site_name: site_name, url: url }); category = "welcome"; } send_email({ subject: subject, body: body, fromname: fallback(settings.organization_name, COMPANY_NAME), from: fallback(settings.organization_email, COMPANY_EMAIL), to: opts.to, cb: opts.cb, category: 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) { var title = SITE_NAME + ": Email verification successful"; return "<DOCTYPE html>\n<html>\n<head>\n<meta http-equiv=\"refresh\" content=\"5;url=" + url + "\" />\n<style>\n* {font-family: sans-serif;}\n</style>\n <title>" + title + "</title>\n</head>\n<body>\n<h1>Email verification successful!</h1>\n<div>\nClick <a href=\"" + url + "\">here</a> if you aren't automatically redirected to <a href=\"" + url + "\">" + SITE_NAME + "</a> within 30 seconds.\n</div>\n</body>\n</html>\n"; } exports.email_verified_successfully = email_verified_successfully; function email_verification_problem(url, problem) { var title = SITE_NAME + ": Email verification problem"; return "<DOCTYPE html>\n<html>\n<head>\n<style>\ndiv, p, h1, h2 {font-family: sans-serif;}\ndiv {margin-top: 1rem;}\n</style>\n <title>" + title + "</title>\n</head>\n<body>\n <h1>" + title + "</h1>\n <div>There was a problem verifying your email address.</div>\n <div>Reason: <code>" + problem + "</code></div>\n <div>\n Continue to <a href=\"" + url + "\">" + SITE_NAME + "</a> or\n contact support: <a href=\"mailto:" + HELP_EMAIL + "\">" + HELP_EMAIL + "</a>.\n </div>\n</body>\n</html>\n "; } exports.email_verification_problem = email_verification_problem; //# sourceMappingURL=email.js.map