|
@@ -2,6 +2,7 @@
|
|
|
#include "common/uuid.hpp"
|
|
#include "common/uuid.hpp"
|
|
|
#include "logging/logger.hpp"
|
|
#include "logging/logger.hpp"
|
|
|
#include "runner/imap/imap_client.hpp"
|
|
#include "runner/imap/imap_client.hpp"
|
|
|
|
|
+#include "runner/smtp/smtp_client.hpp"
|
|
|
#ifdef MYSQL_SUPPORT
|
|
#ifdef MYSQL_SUPPORT
|
|
|
#include "runner/mysql/mysql_client.hpp"
|
|
#include "runner/mysql/mysql_client.hpp"
|
|
|
#endif
|
|
#endif
|
|
@@ -3707,6 +3708,183 @@ static void setupImapAPI(JSContext* ctx, JSValue smartbotic) {
|
|
|
JS_SetPropertyStr(ctx, smartbotic, "imap", imap);
|
|
JS_SetPropertyStr(ctx, smartbotic, "imap", imap);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Reads a string property, leaving the value untouched when it is absent.
|
|
|
|
|
+static std::string smtpStringProp(JSContext* ctx, JSValue obj, const char* name) {
|
|
|
|
|
+ std::string out;
|
|
|
|
|
+ JSValue val = JS_GetPropertyStr(ctx, obj, name);
|
|
|
|
|
+ if (JS_IsString(val)) {
|
|
|
|
|
+ const char* c = JS_ToCString(ctx, val);
|
|
|
|
|
+ if (c) { out = c; JS_FreeCString(ctx, c); }
|
|
|
|
|
+ }
|
|
|
|
|
+ JS_FreeValue(ctx, val);
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// A recipient list is written either as one address or as an array of them,
|
|
|
|
|
+// since sending to a single person is the common case and should not need [].
|
|
|
|
|
+static std::vector<std::string> smtpAddressList(JSContext* ctx, JSValue obj, const char* name) {
|
|
|
|
|
+ std::vector<std::string> out;
|
|
|
|
|
+ JSValue val = JS_GetPropertyStr(ctx, obj, name);
|
|
|
|
|
+
|
|
|
|
|
+ if (JS_IsString(val)) {
|
|
|
|
|
+ const char* c = JS_ToCString(ctx, val);
|
|
|
|
|
+ if (c) {
|
|
|
|
|
+ if (*c) out.emplace_back(c);
|
|
|
|
|
+ JS_FreeCString(ctx, c);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (JS_IsArray(ctx, val)) {
|
|
|
|
|
+ JSValue len_val = JS_GetPropertyStr(ctx, val, "length");
|
|
|
|
|
+ int32_t len = 0;
|
|
|
|
|
+ JS_ToInt32(ctx, &len, len_val);
|
|
|
|
|
+ JS_FreeValue(ctx, len_val);
|
|
|
|
|
+
|
|
|
|
|
+ for (int32_t i = 0; i < len; i++) {
|
|
|
|
|
+ JSValue item = JS_GetPropertyUint32(ctx, val, i);
|
|
|
|
|
+ const char* c = JS_ToCString(ctx, item);
|
|
|
|
|
+ if (c) {
|
|
|
|
|
+ if (*c) out.emplace_back(c);
|
|
|
|
|
+ JS_FreeCString(ctx, c);
|
|
|
|
|
+ }
|
|
|
|
|
+ JS_FreeValue(ctx, item);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ JS_FreeValue(ctx, val);
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// smtp.send(options) - Send an email
|
|
|
|
|
+static JSValue js_smtp_send(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
|
|
+ if (!script_ctx || !script_ctx->credentials_get_smtp) {
|
|
|
|
|
+ return JS_ThrowInternalError(ctx, "SMTP API not available");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (argc < 1 || !JS_IsObject(argv[0])) {
|
|
|
|
|
+ return JS_ThrowTypeError(ctx, "smtp.send requires an options object");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ JSValue options = argv[0];
|
|
|
|
|
+
|
|
|
|
|
+ const std::string credential_id = smtpStringProp(ctx, options, "credentialId");
|
|
|
|
|
+ if (credential_id.empty()) {
|
|
|
|
|
+ return JS_ThrowTypeError(ctx, "smtp.send requires credentialId");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ smtp::Message message;
|
|
|
|
|
+ message.to = smtpAddressList(ctx, options, "to");
|
|
|
|
|
+ message.cc = smtpAddressList(ctx, options, "cc");
|
|
|
|
|
+ message.bcc = smtpAddressList(ctx, options, "bcc");
|
|
|
|
|
+ message.subject = smtpStringProp(ctx, options, "subject");
|
|
|
|
|
+ message.body = smtpStringProp(ctx, options, "body");
|
|
|
|
|
+ message.reply_to = smtpStringProp(ctx, options, "replyTo");
|
|
|
|
|
+
|
|
|
|
|
+ JSValue html_val = JS_GetPropertyStr(ctx, options, "html");
|
|
|
|
|
+ message.html = JS_ToBool(ctx, html_val) == 1;
|
|
|
|
|
+ JS_FreeValue(ctx, html_val);
|
|
|
|
|
+
|
|
|
|
|
+ if (message.to.empty()) {
|
|
|
|
|
+ return JS_ThrowTypeError(ctx, "smtp.send requires at least one recipient in 'to'");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ JSValue attachments_val = JS_GetPropertyStr(ctx, options, "attachments");
|
|
|
|
|
+ if (JS_IsArray(ctx, attachments_val)) {
|
|
|
|
|
+ JSValue len_val = JS_GetPropertyStr(ctx, attachments_val, "length");
|
|
|
|
|
+ int32_t len = 0;
|
|
|
|
|
+ JS_ToInt32(ctx, &len, len_val);
|
|
|
|
|
+ JS_FreeValue(ctx, len_val);
|
|
|
|
|
+
|
|
|
|
|
+ for (int32_t i = 0; i < len; i++) {
|
|
|
|
|
+ JSValue item = JS_GetPropertyUint32(ctx, attachments_val, i);
|
|
|
|
|
+ if (JS_IsObject(item)) {
|
|
|
|
|
+ smtp::Attachment attachment;
|
|
|
|
|
+ attachment.filename = smtpStringProp(ctx, item, "filename");
|
|
|
|
|
+ attachment.mime_type = smtpStringProp(ctx, item, "mimeType");
|
|
|
|
|
+ attachment.content_base64 = smtpStringProp(ctx, item, "contentBase64");
|
|
|
|
|
+ if (!attachment.filename.empty() && !attachment.content_base64.empty()) {
|
|
|
|
|
+ message.attachments.push_back(std::move(attachment));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ JS_FreeValue(ctx, item);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ JS_FreeValue(ctx, attachments_val);
|
|
|
|
|
+
|
|
|
|
|
+ auto cred_result = script_ctx->credentials_get_smtp(credential_id);
|
|
|
|
|
+ if (cred_result.failed()) {
|
|
|
|
|
+ return JS_ThrowInternalError(ctx, "Failed to get SMTP credentials: %s",
|
|
|
|
|
+ cred_result.error().message().c_str());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const auto& cred = cred_result.value();
|
|
|
|
|
+ smtp::SmtpCredentials smtp_creds{cred.host, cred.port, cred.username, cred.password,
|
|
|
|
|
+ cred.security, cred.from_address, cred.from_name};
|
|
|
|
|
+
|
|
|
|
|
+ // The credential names the account, but a workflow may legitimately send as
|
|
|
|
|
+ // a different address on it, so an explicit sender overrides the stored one.
|
|
|
|
|
+ const std::string from_override = smtpStringProp(ctx, options, "from");
|
|
|
|
|
+ if (!from_override.empty()) {
|
|
|
|
|
+ smtp_creds.from_address = from_override;
|
|
|
|
|
+ }
|
|
|
|
|
+ const std::string from_name_override = smtpStringProp(ctx, options, "fromName");
|
|
|
|
|
+ if (!from_name_override.empty()) {
|
|
|
|
|
+ smtp_creds.from_name = from_name_override;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ smtp::SmtpClient client(smtp_creds);
|
|
|
|
|
+ auto send_result = client.send(message);
|
|
|
|
|
+ if (send_result.failed()) {
|
|
|
|
|
+ return JS_ThrowInternalError(ctx, "%s", send_result.error().message().c_str());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ JSValue result = JS_NewObject(ctx);
|
|
|
|
|
+ JS_SetPropertyStr(ctx, result, "success", JS_TRUE);
|
|
|
|
|
+ JS_SetPropertyStr(ctx, result, "messageId",
|
|
|
|
|
+ JS_NewString(ctx, send_result.value().message_id.c_str()));
|
|
|
|
|
+ JS_SetPropertyStr(ctx, result, "accepted",
|
|
|
|
|
+ JS_NewInt32(ctx, static_cast<int32_t>(
|
|
|
|
|
+ message.to.size() + message.cc.size() + message.bcc.size())));
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// A node that hands JSON.parse something that is not JSON gets QuickJS's own
|
|
|
|
|
+// "SyntaxError: unexpected token: '<'", which names neither the value nor where
|
|
|
|
|
+// it came from. Since almost every such value is a server's XML or HTML reply,
|
|
|
|
|
+// the parser is wrapped once for every node so the failure describes what
|
|
|
|
|
+// arrived instead of only which character stopped it.
|
|
|
|
|
+static const char* kJsonParsePrelude = R"JS(
|
|
|
|
|
+(function () {
|
|
|
|
|
+ var originalParse = JSON.parse;
|
|
|
|
|
+ JSON.parse = function (text, reviver) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return originalParse(text, reviver);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ var raw = typeof text === 'string' ? text : String(text);
|
|
|
|
|
+ var trimmed = raw.replace(/^\s+/, '');
|
|
|
|
|
+ var kind = '';
|
|
|
|
|
+ if (trimmed.indexOf('<!DOCTYPE') === 0 || trimmed.indexOf('<html') === 0) {
|
|
|
|
|
+ kind = ' The value is an HTML page, which usually means the request was redirected, rejected or sent to the wrong address.';
|
|
|
|
|
+ } else if (trimmed.indexOf('<') === 0) {
|
|
|
|
|
+ kind = ' The value is XML, not JSON.';
|
|
|
|
|
+ } else if (raw.length === 0) {
|
|
|
|
|
+ kind = ' The value is empty.';
|
|
|
|
|
+ }
|
|
|
|
|
+ var snippet = raw.substring(0, 200).replace(/\s+/g, ' ');
|
|
|
|
|
+ throw new SyntaxError('JSON.parse failed: ' + err.message + '.' + kind +
|
|
|
|
|
+ ' Received ' + raw.length + ' characters starting: ' + snippet);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+})();
|
|
|
|
|
+)JS";
|
|
|
|
|
+
|
|
|
|
|
+// Setup SMTP API on smartbotic namespace
|
|
|
|
|
+static void setupSmtpAPI(JSContext* ctx, JSValue smartbotic) {
|
|
|
|
|
+ JSValue smtp_obj = JS_NewObject(ctx);
|
|
|
|
|
+ JS_SetPropertyStr(ctx, smtp_obj, "send", JS_NewCFunction(ctx, js_smtp_send, "send", 1));
|
|
|
|
|
+ JS_SetPropertyStr(ctx, smartbotic, "smtp", smtp_obj);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// Setup credentials API on smartbotic namespace
|
|
// Setup credentials API on smartbotic namespace
|
|
|
static void setupCredentialsAPI(JSContext* ctx, JSValue smartbotic) {
|
|
static void setupCredentialsAPI(JSContext* ctx, JSValue smartbotic) {
|
|
|
JSValue credentials = JS_NewObject(ctx);
|
|
JSValue credentials = JS_NewObject(ctx);
|
|
@@ -4032,6 +4210,7 @@ ScriptResult ScriptEngine::execute(const std::string& script, const ScriptContex
|
|
|
setupStorageAPI(context_, smartbotic);
|
|
setupStorageAPI(context_, smartbotic);
|
|
|
setupCredentialsAPI(context_, smartbotic);
|
|
setupCredentialsAPI(context_, smartbotic);
|
|
|
setupImapAPI(context_, smartbotic);
|
|
setupImapAPI(context_, smartbotic);
|
|
|
|
|
+ setupSmtpAPI(context_, smartbotic);
|
|
|
#ifdef MYSQL_SUPPORT
|
|
#ifdef MYSQL_SUPPORT
|
|
|
setupMysqlAPI(context_, smartbotic);
|
|
setupMysqlAPI(context_, smartbotic);
|
|
|
#endif
|
|
#endif
|
|
@@ -4041,6 +4220,19 @@ ScriptResult ScriptEngine::execute(const std::string& script, const ScriptContex
|
|
|
}
|
|
}
|
|
|
JS_FreeValue(context_, smartbotic);
|
|
JS_FreeValue(context_, smartbotic);
|
|
|
|
|
|
|
|
|
|
+ JSValue prelude = JS_Eval(context_, kJsonParsePrelude, strlen(kJsonParsePrelude),
|
|
|
|
|
+ "<prelude>", JS_EVAL_TYPE_GLOBAL);
|
|
|
|
|
+ if (JS_IsException(prelude)) {
|
|
|
|
|
+ // The prelude only improves reporting, so a failure to install it must
|
|
|
|
|
+ // not stop the node from running.
|
|
|
|
|
+ JSValue err = JS_GetException(context_);
|
|
|
|
|
+ const char* err_str = JS_ToCString(context_, err);
|
|
|
|
|
+ LOG_WARN("Could not install the JSON.parse prelude: {}", err_str ? err_str : "unknown error");
|
|
|
|
|
+ if (err_str) JS_FreeCString(context_, err_str);
|
|
|
|
|
+ JS_FreeValue(context_, err);
|
|
|
|
|
+ }
|
|
|
|
|
+ JS_FreeValue(context_, prelude);
|
|
|
|
|
+
|
|
|
// Set up module.exports for CommonJS compatibility
|
|
// Set up module.exports for CommonJS compatibility
|
|
|
JSValue module_obj = JS_NewObject(context_);
|
|
JSValue module_obj = JS_NewObject(context_);
|
|
|
JSValue exports_obj = JS_NewObject(context_);
|
|
JSValue exports_obj = JS_NewObject(context_);
|
|
@@ -4151,9 +4343,24 @@ ScriptResult ScriptEngine::execute(const std::string& script, const ScriptContex
|
|
|
JSValue exception_str = JS_ToString(context_, exception);
|
|
JSValue exception_str = JS_ToString(context_, exception);
|
|
|
const char* str = JS_ToCString(context_, exception_str);
|
|
const char* str = JS_ToCString(context_, exception_str);
|
|
|
result.error = str ? std::string("Execute error: ") + str : "Execute function threw an exception";
|
|
result.error = str ? std::string("Execute error: ") + str : "Execute function threw an exception";
|
|
|
- LOG_ERROR("Execute exception: {}", result.error);
|
|
|
|
|
JS_FreeCString(context_, str);
|
|
JS_FreeCString(context_, str);
|
|
|
JS_FreeValue(context_, exception_str);
|
|
JS_FreeValue(context_, exception_str);
|
|
|
|
|
+
|
|
|
|
|
+ // Without the stack, an error thrown deep in a node says what went wrong
|
|
|
|
|
+ // but not where, which is the half that makes it fixable.
|
|
|
|
|
+ if (JS_IsObject(exception)) {
|
|
|
|
|
+ JSValue stack = JS_GetPropertyStr(context_, exception, "stack");
|
|
|
|
|
+ if (!JS_IsUndefined(stack) && !JS_IsNull(stack)) {
|
|
|
|
|
+ const char* stack_str = JS_ToCString(context_, stack);
|
|
|
|
|
+ if (stack_str && *stack_str) {
|
|
|
|
|
+ result.error += "\nStack: " + std::string(stack_str);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (stack_str) JS_FreeCString(context_, stack_str);
|
|
|
|
|
+ }
|
|
|
|
|
+ JS_FreeValue(context_, stack);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ LOG_ERROR("Execute exception: {}", result.error);
|
|
|
JS_FreeValue(context_, exception);
|
|
JS_FreeValue(context_, exception);
|
|
|
result.success = false;
|
|
result.success = false;
|
|
|
} else {
|
|
} else {
|
|
@@ -4186,6 +4393,21 @@ ScriptResult ScriptEngine::execute(const std::string& script, const ScriptContex
|
|
|
const char* str = JS_ToCString(context_, promise_result);
|
|
const char* str = JS_ToCString(context_, promise_result);
|
|
|
result.error = str ? str : "Promise rejected";
|
|
result.error = str ? str : "Promise rejected";
|
|
|
JS_FreeCString(context_, str);
|
|
JS_FreeCString(context_, str);
|
|
|
|
|
+
|
|
|
|
|
+ // An async execute reports its failure here rather than as a
|
|
|
|
|
+ // thrown exception, so the stack has to be recovered here too.
|
|
|
|
|
+ if (JS_IsObject(promise_result)) {
|
|
|
|
|
+ JSValue stack = JS_GetPropertyStr(context_, promise_result, "stack");
|
|
|
|
|
+ if (!JS_IsUndefined(stack) && !JS_IsNull(stack)) {
|
|
|
|
|
+ const char* stack_str = JS_ToCString(context_, stack);
|
|
|
|
|
+ if (stack_str && *stack_str) {
|
|
|
|
|
+ result.error += "\nStack: " + std::string(stack_str);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (stack_str) JS_FreeCString(context_, stack_str);
|
|
|
|
|
+ }
|
|
|
|
|
+ JS_FreeValue(context_, stack);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
JS_FreeValue(context_, promise_result);
|
|
JS_FreeValue(context_, promise_result);
|
|
|
result.success = false;
|
|
result.success = false;
|
|
|
} else if (state == JS_PROMISE_FULFILLED) {
|
|
} else if (state == JS_PROMISE_FULFILLED) {
|