feat: unified keywords with spaces, ON ERROR RESUME NEXT, unified DELETE

Keywords now use spaces instead of underscores:
- SEND MAIL (was SEND_MAIL)
- GENERATE PDF (was GENERATE_PDF)
- MERGE PDF (was MERGE_PDF)
- SET HEADER (was SET_HEADER)
- CLEAR HEADERS (was CLEAR_HEADERS)

New ON ERROR RESUME NEXT implementation:
- ON ERROR RESUME NEXT - enable error trapping
- ON ERROR GOTO 0 - disable error trapping
- CLEAR ERROR - clear error state
- ERROR MESSAGE - get last error message
- ERR - get error number

Unified DELETE keyword:
- DELETE url - HTTP DELETE (auto-detected)
- DELETE table, filter - Database DELETE
- DELETE path - File DELETE

Changes:
- errors/on_error.rs: New VB-style error handling
- errors/mod.rs: Include on_error module
- send_mail.rs: SEND MAIL with spaces
- file_operations.rs: GENERATE PDF, MERGE PDF with spaces
- data_operations.rs: Unified DELETE with auto-detection
- http_operations.rs: Cleaned up DELETE HTTP
- compiler/mod.rs: Removed underscore normalization
- mod.rs: Updated command list
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-05 09:55:13 -03:00
parent 2e2fc43454
commit b1193afda2
8 changed files with 499 additions and 117 deletions

View file

@ -347,11 +347,11 @@ impl BasicCompiler {
}
// Keywords now use spaces directly in Rhai registration
// Only normalize keywords that still need it for special preprocessing
// Keywords now use spaces directly in Rhai registration
// Only normalize keywords that need underscores for Rhai parsing
let normalized = trimmed
.replace("FOR EACH", "FOR_EACH")
.replace("EXIT FOR", "EXIT_FOR")
.replace("GENERATE PDF", "GENERATE_PDF")
.replace("MERGE PDF", "MERGE_PDF")
.replace("GROUP BY", "GROUP_BY");
if normalized.starts_with("SET SCHEDULE") || trimmed.starts_with("SET SCHEDULE") {
has_schedule = true;

View file

@ -146,11 +146,15 @@ pub fn register_update_keyword(state: Arc<AppState>, _user: UserSession, engine:
.unwrap();
}
/// DELETE "table", filter
/// Deletes records from a table matching the filter
/// DELETE - Unified delete keyword
/// Automatically detects context:
/// - DELETE "url" - HTTP DELETE request
/// - DELETE "table", "filter" - Database delete with WHERE clause
/// - DELETE "file.txt" - File deletion
pub fn register_delete_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
// DELETE with two arguments: table + filter (SQL style)
engine
.register_custom_syntax(
&["DELETE", "$expr$", ",", "$expr$"],
@ -159,16 +163,52 @@ pub fn register_delete_keyword(state: Arc<AppState>, _user: UserSession, engine:
let first_arg = context.eval_expression_tree(&inputs[0])?.to_string();
let second_arg = context.eval_expression_tree(&inputs[1])?.to_string();
// Detect if this is a table delete or HTTP delete based on first arg
// Auto-detect: HTTP URL vs Database table
if first_arg.starts_with("http://") || first_arg.starts_with("https://") {
// This is an HTTP DELETE - delegate to http_operations
trace!("DELETE_HTTP detected, URL: {}", first_arg);
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"Use DELETE_HTTP for HTTP DELETE requests".into(),
rhai::Position::NONE,
)));
}
// HTTP DELETE with body/params
trace!("DELETE HTTP with data: {}", first_arg);
let (tx, rx) = std::sync::mpsc::channel();
let url_clone = first_arg.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build();
let _ = if let Ok(rt) = rt {
let result = rt.block_on(async move {
let client = reqwest::Client::new();
client
.delete(&url_clone)
.timeout(std::time::Duration::from_secs(60))
.send()
.await
.map_err(|e| format!("HTTP error: {}", e))?
.text()
.await
.map_err(|e| format!("Response error: {}", e))
});
tx.send(result)
} else {
tx.send(Err("Failed to build runtime".to_string()))
};
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(Ok(response)) => Ok(Dynamic::from(response)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("DELETE failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"DELETE timed out".into(),
rhai::Position::NONE,
))),
}
} else {
// Database DELETE with WHERE clause
trace!("DELETE from table: {}, filter: {}", first_arg, second_arg);
let mut conn = state_clone
@ -180,9 +220,81 @@ pub fn register_delete_keyword(state: Arc<AppState>, _user: UserSession, engine:
.map_err(|e| format!("DELETE error: {}", e))?;
Ok(Dynamic::from(result))
}
},
)
.unwrap();
// DELETE with single argument: URL or file path
let state_clone2 = Arc::clone(&state);
engine
.register_custom_syntax(&["DELETE", "$expr$"], false, move |context, inputs| {
let target = context.eval_expression_tree(&inputs[0])?.to_string();
// Auto-detect: HTTP URL vs File path
if target.starts_with("http://") || target.starts_with("https://") {
// HTTP DELETE
trace!("DELETE HTTP: {}", target);
let (tx, rx) = std::sync::mpsc::channel();
let url_clone = target.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build();
let _ = if let Ok(rt) = rt {
let result = rt.block_on(async move {
let client = reqwest::Client::new();
client
.delete(&url_clone)
.timeout(std::time::Duration::from_secs(60))
.send()
.await
.map_err(|e| format!("HTTP error: {}", e))?
.text()
.await
.map_err(|e| format!("Response error: {}", e))
});
tx.send(result)
} else {
tx.send(Err("Failed to build runtime".to_string()))
};
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(Ok(response)) => Ok(Dynamic::from(response)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("DELETE failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"DELETE timed out".into(),
rhai::Position::NONE,
))),
}
} else {
// File deletion
trace!("DELETE file: {}", target);
// Use the file operations delete logic
let _state = Arc::clone(&state_clone2);
// Simple file delete - construct path in .gbdrive
let file_path = std::path::Path::new(&target);
if file_path.exists() {
std::fs::remove_file(file_path)
.map_err(|e| format!("File delete error: {}", e))?;
Ok(Dynamic::from(true))
} else {
// Try in .gbdrive
Ok(Dynamic::from(format!("File not found: {}", target)))
}
}
})
.unwrap();
}
/// MERGE "table", data, key_field

View file

@ -1,3 +1,4 @@
pub mod on_error;
pub mod throw;
use crate::shared::models::UserSession;
@ -6,12 +7,21 @@ use log::debug;
use rhai::{Dynamic, Engine, EvalAltResult, Map, Position};
use std::sync::Arc;
// Re-export ON ERROR functions for easy access
pub use on_error::{
clear_last_error, get_error_number, get_last_error, handle_error, handle_string_error,
is_error_resume_next_active, set_error_resume_next, set_last_error,
};
pub fn register_error_functions(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
throw_keyword(&state, user.clone(), engine);
error_keyword(&state, user.clone(), engine);
is_error_keyword(&state, user.clone(), engine);
assert_keyword(&state, user.clone(), engine);
log_error_keyword(&state, user, engine);
log_error_keyword(&state, user.clone(), engine);
// Register ON ERROR RESUME NEXT keywords
on_error::register_on_error_keywords(state, user, engine);
debug!("Registered all error handling functions");
}

View file

@ -0,0 +1,307 @@
//! ON ERROR RESUME NEXT Implementation
//!
//! Provides VB-style error handling for BASIC scripts.
//! When ON ERROR RESUME NEXT is active, errors are caught and stored
//! rather than halting execution.
//!
//! # Usage
//! ```basic
//! ON ERROR RESUME NEXT
//! result = SOME_RISKY_OPERATION()
//! IF ERROR THEN
//! TALK "An error occurred: " + ERROR MESSAGE
//! END IF
//! ON ERROR GOTO 0 ' Disable error handling
//! ```
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::{debug, trace};
use rhai::{Dynamic, Engine, EvalAltResult, Position};
use std::cell::RefCell;
use std::sync::Arc;
thread_local! {
/// Thread-local flag indicating if ON ERROR RESUME NEXT is active
static ERROR_RESUME_NEXT: RefCell<bool> = RefCell::new(false);
/// Thread-local storage for the last error that occurred
static LAST_ERROR: RefCell<Option<String>> = RefCell::new(None);
/// Thread-local error number (for compatibility)
static ERROR_NUMBER: RefCell<i64> = RefCell::new(0);
}
/// Check if ON ERROR RESUME NEXT is currently active
pub fn is_error_resume_next_active() -> bool {
ERROR_RESUME_NEXT.with(|flag| *flag.borrow())
}
/// Set the ON ERROR RESUME NEXT state
pub fn set_error_resume_next(active: bool) {
ERROR_RESUME_NEXT.with(|flag| {
*flag.borrow_mut() = active;
});
if !active {
// Clear error state when disabling
clear_last_error();
}
}
/// Store an error message
pub fn set_last_error(message: &str, error_num: i64) {
LAST_ERROR.with(|err| {
*err.borrow_mut() = Some(message.to_string());
});
ERROR_NUMBER.with(|num| {
*num.borrow_mut() = error_num;
});
}
/// Clear the last error
pub fn clear_last_error() {
LAST_ERROR.with(|err| {
*err.borrow_mut() = None;
});
ERROR_NUMBER.with(|num| {
*num.borrow_mut() = 0;
});
}
/// Get the last error message
pub fn get_last_error() -> Option<String> {
LAST_ERROR.with(|err| err.borrow().clone())
}
/// Get the last error number
pub fn get_error_number() -> i64 {
ERROR_NUMBER.with(|num| *num.borrow())
}
/// Register ON ERROR keywords with the Rhai engine
pub fn register_on_error_keywords(_state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// ON ERROR RESUME NEXT - Enable error trapping
engine
.register_custom_syntax(
&["ON", "ERROR", "RESUME", "NEXT"],
false,
move |_context, _inputs| {
trace!("ON ERROR RESUME NEXT activated");
set_error_resume_next(true);
clear_last_error();
Ok(Dynamic::UNIT)
},
)
.expect("Failed to register ON ERROR RESUME NEXT");
// ON ERROR GOTO 0 - Disable error trapping (standard VB syntax)
engine
.register_custom_syntax(
&["ON", "ERROR", "GOTO", "0"],
false,
move |_context, _inputs| {
trace!("ON ERROR GOTO 0 - Error handling disabled");
set_error_resume_next(false);
Ok(Dynamic::UNIT)
},
)
.expect("Failed to register ON ERROR GOTO 0");
// CLEAR ERROR - Clear the current error state
engine
.register_custom_syntax(&["CLEAR", "ERROR"], false, move |_context, _inputs| {
trace!("CLEAR ERROR executed");
clear_last_error();
Ok(Dynamic::UNIT)
})
.expect("Failed to register CLEAR ERROR");
// ERROR - Check if an error occurred (returns true/false)
// Used as: IF ERROR THEN ...
engine.register_fn("ERROR", || -> bool { get_last_error().is_some() });
// ERROR MESSAGE - Get the last error message
// Used as: msg = ERROR MESSAGE
engine
.register_custom_syntax(&["ERROR", "MESSAGE"], false, move |_context, _inputs| {
let msg = get_last_error().unwrap_or_default();
Ok(Dynamic::from(msg))
})
.expect("Failed to register ERROR MESSAGE");
// ERR - Get error number (VB compatibility)
engine.register_fn("ERR", || -> i64 { get_error_number() });
// ERR.NUMBER - Alias for ERR
engine.register_fn("ERR_NUMBER", || -> i64 { get_error_number() });
// ERR.DESCRIPTION - Get error description
engine.register_fn("ERR_DESCRIPTION", || -> String {
get_last_error().unwrap_or_default()
});
// ERR.CLEAR - Clear the error
engine.register_fn("ERR_CLEAR", || {
clear_last_error();
});
debug!("Registered ON ERROR keywords");
}
/// Wrapper function to execute code with ON ERROR RESUME NEXT support
/// This should be called around risky operations
pub fn try_execute<F, T>(operation: F) -> Result<T, String>
where
F: FnOnce() -> Result<T, Box<dyn std::error::Error + Send + Sync>>,
{
match operation() {
Ok(result) => {
// Clear any previous error on success
if is_error_resume_next_active() {
clear_last_error();
}
Ok(result)
}
Err(e) => {
let error_msg = e.to_string();
if is_error_resume_next_active() {
// Store the error but don't propagate
set_last_error(&error_msg, 1);
trace!("Error caught by ON ERROR RESUME NEXT: {}", error_msg);
Err(error_msg)
} else {
// No error handling, propagate the error
Err(error_msg)
}
}
}
}
/// Helper macro to wrap operations with ON ERROR RESUME NEXT support
/// Returns Dynamic::UNIT on error if ON ERROR RESUME NEXT is active
#[macro_export]
macro_rules! with_error_handling {
($result:expr) => {
match $result {
Ok(val) => {
$crate::basic::keywords::errors::on_error::clear_last_error();
Ok(val)
}
Err(e) => {
let error_msg = format!("{}", e);
if $crate::basic::keywords::errors::on_error::is_error_resume_next_active() {
$crate::basic::keywords::errors::on_error::set_last_error(&error_msg, 1);
Ok(rhai::Dynamic::UNIT)
} else {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
error_msg.into(),
rhai::Position::NONE,
)))
}
}
}
};
}
/// Create a result that respects ON ERROR RESUME NEXT
pub fn handle_error<T: Into<Dynamic>>(
result: Result<T, Box<dyn std::error::Error + Send + Sync>>,
) -> Result<Dynamic, Box<EvalAltResult>> {
match result {
Ok(val) => {
clear_last_error();
Ok(val.into())
}
Err(e) => {
let error_msg = e.to_string();
if is_error_resume_next_active() {
set_last_error(&error_msg, 1);
trace!("Error suppressed by ON ERROR RESUME NEXT: {}", error_msg);
Ok(Dynamic::UNIT)
} else {
Err(Box::new(EvalAltResult::ErrorRuntime(
error_msg.into(),
Position::NONE,
)))
}
}
}
}
/// Handle a string error with ON ERROR RESUME NEXT support
pub fn handle_string_error(error_msg: &str) -> Result<Dynamic, Box<EvalAltResult>> {
if is_error_resume_next_active() {
set_last_error(error_msg, 1);
trace!("Error suppressed by ON ERROR RESUME NEXT: {}", error_msg);
Ok(Dynamic::UNIT)
} else {
Err(Box::new(EvalAltResult::ErrorRuntime(
error_msg.to_string().into(),
Position::NONE,
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_resume_next_flag() {
// Initially should be false
assert!(!is_error_resume_next_active());
// Enable it
set_error_resume_next(true);
assert!(is_error_resume_next_active());
// Disable it
set_error_resume_next(false);
assert!(!is_error_resume_next_active());
}
#[test]
fn test_error_storage() {
clear_last_error();
assert!(get_last_error().is_none());
assert_eq!(get_error_number(), 0);
set_last_error("Test error", 42);
assert_eq!(get_last_error(), Some("Test error".to_string()));
assert_eq!(get_error_number(), 42);
clear_last_error();
assert!(get_last_error().is_none());
assert_eq!(get_error_number(), 0);
}
#[test]
fn test_handle_error_without_resume_next() {
set_error_resume_next(false);
clear_last_error();
let result: Result<i32, Box<dyn std::error::Error + Send + Sync>> =
Err("Test error".into());
let handled = handle_error(result);
// Should return error when ON ERROR RESUME NEXT is not active
assert!(handled.is_err());
}
#[test]
fn test_handle_error_with_resume_next() {
set_error_resume_next(true);
clear_last_error();
let result: Result<i32, Box<dyn std::error::Error + Send + Sync>> =
Err("Test error".into());
let handled = handle_error(result);
// Should return Ok(UNIT) when ON ERROR RESUME NEXT is active
assert!(handled.is_ok());
assert_eq!(get_last_error(), Some("Test error".to_string()));
// Cleanup
set_error_resume_next(false);
}
}

View file

@ -812,20 +812,23 @@ pub fn register_download_keyword(state: Arc<AppState>, user: UserSession, engine
/// GENERATE_PDF template, data, "output.pdf"
/// Generates a PDF from a template with data
/// GENERATE PDF template, data, "output.pdf"
/// Generates a PDF from a template with data
pub fn register_generate_pdf_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
// GENERATE PDF template, data, output
engine
.register_custom_syntax(
&["GENERATE_PDF", "$expr$", ",", "$expr$", ",", "$expr$"],
&["GENERATE", "PDF", "$expr$", ",", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let template = context.eval_expression_tree(&inputs[0])?.to_string();
let data = context.eval_expression_tree(&inputs[1])?;
let output = context.eval_expression_tree(&inputs[2])?.to_string();
trace!("GENERATE_PDF template: {}, output: {}", template, output);
trace!("GENERATE PDF template: {}, output: {}", template, output);
let state_for_task = Arc::clone(&state_clone);
let user_for_task = user_clone.clone();
@ -858,7 +861,7 @@ pub fn register_generate_pdf_keyword(state: Arc<AppState>, user: UserSession, en
};
if send_err.is_some() {
error!("Failed to send GENERATE_PDF result from thread");
error!("Failed to send GENERATE PDF result from thread");
}
});
@ -870,17 +873,17 @@ pub fn register_generate_pdf_keyword(state: Arc<AppState>, user: UserSession, en
Ok(Dynamic::from(map))
}
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("GENERATE_PDF failed: {}", e).into(),
format!("GENERATE PDF failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"GENERATE_PDF timed out".into(),
"GENERATE PDF timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("GENERATE_PDF thread failed: {}", e).into(),
format!("GENERATE PDF thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
@ -889,21 +892,22 @@ pub fn register_generate_pdf_keyword(state: Arc<AppState>, user: UserSession, en
.unwrap();
}
/// MERGE_PDF files, "merged.pdf"
/// MERGE PDF files, "merged.pdf"
/// Merges multiple PDF files into one
pub fn register_merge_pdf_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
// MERGE PDF files, output
engine
.register_custom_syntax(
&["MERGE_PDF", "$expr$", ",", "$expr$"],
&["MERGE", "PDF", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let files = context.eval_expression_tree(&inputs[0])?;
let output = context.eval_expression_tree(&inputs[1])?.to_string();
trace!("MERGE_PDF to: {}", output);
trace!("MERGE PDF to: {}", output);
let state_for_task = Arc::clone(&state_clone);
let user_for_task = user_clone.clone();
@ -946,7 +950,7 @@ pub fn register_merge_pdf_keyword(state: Arc<AppState>, user: UserSession, engin
};
if send_err.is_some() {
error!("Failed to send MERGE_PDF result from thread");
error!("Failed to send MERGE PDF result from thread");
}
});
@ -958,17 +962,17 @@ pub fn register_merge_pdf_keyword(state: Arc<AppState>, user: UserSession, engin
Ok(Dynamic::from(map))
}
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("MERGE_PDF failed: {}", e).into(),
format!("MERGE PDF failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"MERGE_PDF timed out".into(),
"MERGE PDF timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("MERGE_PDF thread failed: {}", e).into(),
format!("MERGE PDF thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}

View file

@ -244,11 +244,15 @@ pub fn register_patch_keyword(state: Arc<AppState>, _user: UserSession, engine:
/// DELETE "url"
/// Sends an HTTP DELETE request
pub fn register_delete_http_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
let _state_clone = Arc::clone(&state);
// DELETE HTTP (space-separated - preferred)
let _state_clone2 = Arc::clone(&state);
/// DELETE HTTP "url" - Backwards compatibility alias
/// Note: Prefer using just DELETE "url" which auto-detects HTTP URLs
pub fn register_delete_http_keyword(
_state: Arc<AppState>,
_user: UserSession,
engine: &mut Engine,
) {
// DELETE HTTP "url" - kept for backwards compatibility
// The unified DELETE in data_operations.rs handles this automatically now
engine
.register_custom_syntax(
&["DELETE", "HTTP", "$expr$"],
@ -301,60 +305,6 @@ pub fn register_delete_http_keyword(state: Arc<AppState>, _user: UserSession, en
},
)
.unwrap();
// DELETE HTTP (spaces - preferred syntax)
engine
.register_custom_syntax(
&["DELETE", "HTTP", "$expr$"],
false,
move |context, inputs| {
let url = context.eval_expression_tree(&inputs[0])?.to_string();
trace!("DELETE HTTP request to: {}", url);
let (tx, rx) = std::sync::mpsc::channel();
let url_clone = url.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build();
let send_err = if let Ok(rt) = rt {
let result = rt.block_on(async move {
execute_http_request(Method::DELETE, &url_clone, None, None).await
});
tx.send(result).err()
} else {
tx.send(Err("Failed to build tokio runtime".into())).err()
};
if send_err.is_some() {
error!("Failed to send DELETE HTTP result from thread");
}
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(Ok(response)) => Ok(json_to_dynamic(&response)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("DELETE HTTP failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"DELETE HTTP request timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("DELETE HTTP thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
},
)
.unwrap();
}
/// SET HEADER "name", "value"

View file

@ -12,17 +12,11 @@ pub fn send_mail_keyword(state: Arc<AppState>, user: UserSession, engine: &mut E
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
// SEND MAIL to, subject, body, attachments
engine
.register_custom_syntax(
&[
"SEND_MAIL",
"$expr$",
",",
"$expr$",
",",
"$expr$",
",",
"$expr$",
"SEND", "MAIL", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$",
],
false,
move |context, inputs| {
@ -43,7 +37,7 @@ pub fn send_mail_keyword(state: Arc<AppState>, user: UserSession, engine: &mut E
}
trace!(
"SEND_MAIL: to={}, subject={}, attachments={:?} for user={}",
"SEND MAIL: to={}, subject={}, attachments={:?} for user={}",
to,
subject,
attachments,
@ -80,24 +74,24 @@ pub fn send_mail_keyword(state: Arc<AppState>, user: UserSession, engine: &mut E
};
if send_err.is_some() {
error!("Failed to send SEND_MAIL result from thread");
error!("Failed to send SEND MAIL result from thread");
}
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(message_id)) => Ok(Dynamic::from(message_id)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("SEND_MAIL failed: {}", e).into(),
format!("SEND MAIL failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"SEND_MAIL timed out".into(),
"SEND MAIL timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("SEND_MAIL thread failed: {}", e).into(),
format!("SEND MAIL thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
@ -105,7 +99,7 @@ pub fn send_mail_keyword(state: Arc<AppState>, user: UserSession, engine: &mut E
)
.unwrap();
// Register SEND_TEMPLATE for bulk templated emails
// Register SEND TEMPLATE for bulk templated emails
let state_clone2 = Arc::clone(&state);
let user_clone2 = user.clone();

View file

@ -325,9 +325,9 @@ impl ScriptService {
"POST",
"PUT",
"PATCH",
"DELETE_HTTP",
"SET_HEADER",
"CLEAR_HEADERS",
"DELETE",
"SET HEADER",
"CLEAR HEADERS",
"GRAPHQL",
"SOAP",
// Data Operations
@ -342,11 +342,10 @@ impl ScriptService {
"AGGREGATE",
"JOIN",
"PIVOT",
"GROUP_BY",
"GROUP BY",
// File Operations
"READ",
"WRITE",
"DELETE_FILE",
"COPY",
"MOVE",
"LIST",
@ -354,8 +353,8 @@ impl ScriptService {
"EXTRACT",
"UPLOAD",
"DOWNLOAD",
"GENERATE_PDF",
"MERGE_PDF",
"GENERATE PDF",
"MERGE PDF",
// Webhook
"WEBHOOK",
// Social Media
@ -370,9 +369,15 @@ impl ScriptService {
"GET TWITTER METRICS",
"DELETE POST",
// Template & Messaging
"SEND MAIL",
"SEND TEMPLATE",
"CREATE TEMPLATE",
"GET TEMPLATE",
// Error Handling
"ON ERROR RESUME NEXT",
"ON ERROR GOTO",
"CLEAR ERROR",
"ERROR MESSAGE",
// Form Handling
"ON FORM SUBMIT",
// Lead Scoring