feat(EmailWorkers): Implement email worker functionality
Closes #274
Looking for initial comments so I can wrap up this implementation. I've successfully validated reply and forward functionality.
example handler:
use uuid::Uuid;
use worker::*;
#[event(email)]
async fn main(message: EmailMessage, _env: Env, _ctx: Context) -> Result<()> {
let message_id = message.headers().get("Message-ID")?.unwrap();
let msg = format!(
"From: Cloudflare bot <{}>
To: Toon <{}>
In-Reply-To: {}
Message-ID: <{}@redacted.com>
Subject: Email well received!
I've parsed the mail!
",
message.to_email(),
message.from_email(),
message_id,
Uuid::new_v4()
);
let new_message = EmailMessage::new(&message.to_email(), &message.from_email(), &msg)?;
message.reply(new_message).await?;
Ok(())
}
Does this part need to be a specific format? Or can it be any string?
let msg = format!(
"From: Cloudflare bot <{}>
To: Toon <{}>
In-Reply-To: {}
Message-ID: <{}@redacted.com>
Subject: Email well received!
I've parsed the mail!
"
What I am wondering is if it needs a specific format, maybe it should use the type system to enforce it?
If it's just any old string, then disregard
What about an API like this:
use uuid::Uuid;
use worker::*;
#[event(email)]
async fn main(message: EmailMessage, _env: Env, _ctx: Context) -> Result<()> {
let new_message = EmailMessage::try_from(
RawEmailMessage::builder()
.from_name("From Name")
.from_email(&message.to_email())
.to_name("To Name")
.to_email(&message.from_email())
.subject("Email well received!")
.date("Sat, 15 Mar 2025 22:06:02 +0000")
.message_id(format!("{}@redacted.com", Uuid::new_v4()))
.in_reply_to(message.headers().get("Message-ID")?.unwrap())
.message("I've parsed the mail!")
.build(),
);
message.reply(new_message).await?;
Ok(())
}
With the type system enforcing mandatory fields at compile time and removing any risk of user formatting errors?
Actually, on second thought, maybe msg should just be left a a string and if the user wants to use a build helper, it can exist in userland as a separate crate.
Actually, on second thought, maybe
msgshould just be left a a string and if the user wants to use abuildhelper, it can exist in userland as a separate crate.
yeah, In my testing not a lot of the existing crates seem to work well, so it might be useful to create a small crate that fills this gap (but should imho not be part of this crate)
This looked promising: https://docs.rs/mail-builder/latest/mail_builder/index.html
This looked promising: https://docs.rs/mail-builder/latest/mail_builder/index.html
Tried this, got a CPU limit exceeded error on Cloudflare. (I think it's related to it adding the Date header, but did not want to spend much more time debugging)
@zebp If you have some time for a review :)
I tried out these changes, and I was able to receive and reply to emails just fine. I also used the mail_parser and mail_builder crates, and I found that they worked pretty well in combination with workers. The one thing I noticed is that you have to explicitly set a date and message id when building a message because otherwise the defaults will panic when it tries to get the date or generate a random id.
It would be nice if functionality for the SendEmail binding could also be added. I tried adding it myself I was also able to send emails that way. I just reused the binding for an EmailMessage and it worked to call send().
I tried out these changes, and I was able to receive and reply to emails just fine. I also used the mail_parser and mail_builder crates, and I found that they worked pretty well in combination with workers. The one thing I noticed is that you have to explicitly set a date and message id when building a message because otherwise the defaults will panic when it tries to get the date or generate a random id.
It would be nice if functionality for the SendEmail binding could also be added. I tried adding it myself I was also able to send emails that way. I just reused the binding for an EmailMessage and it worked to call send().
@jeholliday would you be able to share what you wrote to implement the SendEmail binding? and the other stuff too. It would be really helpful. I am also trying to mainly use the send to other email addresses functionality instead of replying to received email.
@jeholliday would you be able to share what you wrote to implement the
SendEmail binding? and the other stuff too. It would be really helpful. I am also trying to mainly use the send to other email addresses functionality instead of replying to received email.
@devnull03 Sure, sorry it took a few days. I have committed the changes needed to get the SendEmail binding working in https://github.com/jeholliday/workers-rs/commit/f05b750e46d63a4f71ff244d29234421dddbc953
This is an excerpt from where I am successfully using it:
use mail_builder::MessageBuilder;
use uuid::Uuid;
use worker::*;
async fn send_email(subject: &str, body: &str, env: &Env) -> Result<()> {
let from = "<from_email>";
let to = "<to_email>";
let msg = MessageBuilder::new()
.from(from)
.to(to)
.subject(subject)
.date(Date::now().as_millis() / 1000)
.message_id(format!("{}@<my_domain>", Uuid::new_v4()))
.text_body(body)
.write_to_string()
.unwrap();
let msg = EmailMessage::new(from, to, &msg)?;
let seb = env.send_email("SEB")?;
seb.send(msg).await?;
Ok(())
}
This uses a binding named SEB from my workers.toml:
send_email = [
{ name = "SEB", destination_address = "<to_email>" },
]
Edit: BTW I remember it was very annoying to get the uuid and rand crates working, but I don't remember the details. I just have the following in my Cargo.toml to force it to use an older version, but there might be a better solution:
rand = "0.8.5"
getrandom = { version = "0.2.15", features = ["js"] }
uuid = { version = "=1.9", features = ["v4", "js"] }
Also if you are trying to use this branch, make sure you use the worker-build from this branch or it will fail to build: cargo install --path worker-build
I was able to get this to work with MessageBuilder for replying to incoming e-mails but one thing to note is that In-Reply-To must be set and you cannot use .in_reply_to() with the incoming message ID header directly, as MessageBuilder adds surrounding <>.
Instead, you need to set the header manually using a raw header value.
let msg_id = msg.headers().get("Message-ID")?.unwrap();
let reply_msg = MessageBuilder::new()
.header("In-Reply-To", HeaderType::from(Raw::new(msg_id)))