Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add email worker and send email support #624

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions examples/email/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "email"
version = "0.1.0"
edition = "2021"

[package.metadata.release]
release = false

# https://github.com/rustwasm/wasm-pack/issues/1247
[package.metadata.wasm-pack.profile.release]
wasm-opt = false

[lib]
crate-type = ["cdylib"]

[dependencies]
worker = { workspace=true }
console_error_panic_hook = { version = "0.1.1" }
22 changes: 22 additions & 0 deletions examples/email/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use core::str;

use worker::*;

#[event(email)]
async fn email(message: ForwardableEmailMessage, _env: Env, _ctx: Context) -> Result<()> {
console_error_panic_hook::set_once();

let allow_list = ["[email protected]", "[email protected]"];
let from = message.from_envelope();

let raw: Vec<u8> = message.raw_bytes().await?;
let raw = str::from_utf8(&raw)?;
console_log!("Raw email: {}", raw);

if allow_list.contains(&from.as_str()) {
message.forward("[email protected]", None).await?;
} else {
message.set_reject("Address not allowed")?;
}
Ok(())
}
6 changes: 6 additions & 0 deletions examples/email/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name = "email-worker"
main = "build/worker/shim.mjs"
compatibility_date = "2024-08-13"

[build]
command = "cargo install -q worker-build && worker-build --release"
7 changes: 7 additions & 0 deletions worker-build/src/js/shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ class Entrypoint extends WorkerEntrypoint {
async scheduled(event) {
return await imports.scheduled(event, this.env, this.ctx)
}

// For some reason, email events doesn't seem to use WorkerEntrypoint so we get the env and ctx from
// from the function itself.
async email(message, _env, _ctx) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ObsidianMinor do you know why email worker wouldn't use the new interface? Will cron triggers have the same issue?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still haven't figured it out yet - for some reason env is still not probably propagated into the rust layer, and therefore I can't seem to use bindings with it 😅

Copy link
Author

@LuisDuarte1 LuisDuarte1 Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its also quite difficult to debug because email workers can't be run locally 😅

return await imports.email_handler(message, _env, _ctx)
}
}

const EXCLUDE_EXPORT = [
Expand All @@ -33,6 +39,7 @@ const EXCLUDE_EXPORT = [
"fetch",
"queue",
"scheduled",
"email",
"getMemory"
];

Expand Down
41 changes: 41 additions & 0 deletions worker-macros/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub fn expand_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
Start,
#[cfg(feature = "queue")]
Queue,
Email,
}
use HandlerType::*;

Expand All @@ -28,6 +29,7 @@ pub fn expand_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
"respond_with_errors" => {
respond_with_errors = true;
}
"email" => handler_type = Some(Email),
_ => panic!("Invalid attribute: {}", attr),
}
}
Expand Down Expand Up @@ -183,6 +185,45 @@ pub fn expand_macro(attr: TokenStream, item: TokenStream) -> TokenStream {

TokenStream::from(output)
}
Email => {
let input_fn_ident = Ident::new(
&(input_fn.sig.ident.to_string() + "_email_glue"),
input_fn.sig.ident.span(),
);
let wrapper_fn_ident = Ident::new("email_handler", input_fn.sig.ident.span());
// rename the original attributed fn
input_fn.sig.ident = input_fn_ident.clone();

let wrapper_fn = quote! {
pub async fn #wrapper_fn_ident(message: ::worker::worker_sys::ForwardableEmailMessage, env: ::worker::Env, ctx: ::worker::worker_sys::Context) {
// call the original fn
let ctx = worker::Context::new(ctx);
match #input_fn_ident(::worker::ForwardableEmailMessage::from(message), env, ctx).await {
Ok(()) => {},
Err(e) => {
::worker::console_log!("{}", &e);
panic!("{}", e);
}
}
}
};

let wasm_bindgen_code =
wasm_bindgen_macro_support::expand(TokenStream::new().into(), wrapper_fn)
.expect("wasm_bindgen macro failed to expand");

let output = quote! {
#input_fn

mod _worker_email {
use ::worker::{wasm_bindgen, wasm_bindgen_futures};
use super::#input_fn_ident;
#wasm_bindgen_code
}
};

TokenStream::from(output)
}
Start => {
// save original fn name for re-use in the wrapper fn
let input_fn_ident = Ident::new(
Expand Down
2 changes: 2 additions & 0 deletions worker-sys/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod crypto;
mod d1;
mod durable_object;
mod dynamic_dispatcher;
mod email;
mod fetcher;
mod fixed_length_stream;
mod hyperdrive;
Expand All @@ -23,6 +24,7 @@ pub use crypto::*;
pub use d1::*;
pub use durable_object::*;
pub use dynamic_dispatcher::*;
pub use email::*;
pub use fetcher::*;
pub use fixed_length_stream::*;
pub use hyperdrive::*;
Expand Down
64 changes: 64 additions & 0 deletions worker-sys/src/types/email.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use web_sys::{Headers, ReadableStream};

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends=js_sys::Object)]
#[derive(Clone, PartialEq, Eq)]
pub type EmailMessage;

// TODO(lduarte): see if also accepting string is needed
#[wasm_bindgen(constructor, catch)]
pub fn new(from: &str, to: &str, raw: &str) -> Result<EmailMessage, JsValue>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also have a new_from_stream method that takes ReadableStream?


#[wasm_bindgen(method, getter)]
pub fn from(this: &EmailMessage) -> String;

#[wasm_bindgen(method, getter)]
pub fn to(this: &EmailMessage) -> String;
}

#[wasm_bindgen]
extern "C" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add headers field? Should these getters also be on EmailMessage?

#[wasm_bindgen(extends=js_sys::Object)]
#[derive(Clone, PartialEq, Eq)]
pub type ForwardableEmailMessage;

#[wasm_bindgen(method, getter)]
pub fn from(this: &ForwardableEmailMessage) -> String;

#[wasm_bindgen(method, getter)]
pub fn to(this: &ForwardableEmailMessage) -> String;

#[wasm_bindgen(method, getter)]
pub fn raw(this: &ForwardableEmailMessage) -> ReadableStream;

// File size will never pass over 4GB so u32 is enough
#[wasm_bindgen(method, getter, js_name=rawSize)]
pub fn raw_size(this: &ForwardableEmailMessage) -> u32;

#[wasm_bindgen(method, catch, js_name=setReject)]
pub fn set_reject(this: &ForwardableEmailMessage, reason: &str) -> Result<(), JsValue>;

#[wasm_bindgen(method, catch)]
pub fn forward(
this: &ForwardableEmailMessage,
rcpt_to: &str,
headers: Headers,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field should probably be optional, or maybe have forward and forward_with_headers. Or is there no difference between headers being undefined or {} in JavaScript API?

) -> Result<Promise, JsValue>;

#[wasm_bindgen(method, catch)]
pub fn reply(this: &ForwardableEmailMessage, email: EmailMessage) -> Result<Promise, JsValue>;
}

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends=js_sys::Object)]
#[derive(Clone, PartialEq, Eq)]
pub type SendEmail;

#[wasm_bindgen(method, catch)]
pub fn send(this: &SendEmail, email: EmailMessage) -> Result<Promise, JsValue>;

}
Loading
Loading