In this tutorial, I’ll show you how to use Replit Agent to generate a React app and add email-sending functionality to it by leveraging EmailJS and Mailtrap SMTP.
For your convenience, here are the chapters you can jump ahead to:
- Setting up EmailJS and Mailtrap SMTP
- Creating a React contact form with prompts
- Sending emails from the React contact form
- React + EmailJS: security considerations
Disclaimer: The workflow in this article has been prepared and tested by a developer before publication.
Setting up EmailJS
EmailJS is a client-side or front-end solution for sending emails in JavaScript, which can also be used to send emails via TypeScript. The service is super simple:
- You create a free account
- Connect your preferred email service
- Create an email template
- Send the email via your Replit-generated app
To configure EmailJS, follow these steps:
1. Sign up for EmailJS
Go to the EmailJS website and create a free account.
After signing up, in the Notifications section in the account tab, make sure the email matches your sending domain from Mailtrap (e.g. no-reply@my.own.domain).
2. Connect an email service
In the EmailJS dashboard, navigate to Email Services and connect a service. For this demo, we’ll use Mailtrap, since it provides a reliable SMTP service with additional features like in-depth analytics.
After selecting the email service, you’ll need to complete the setup wizard with the following details:
- Service ID – This field will have a pre-generated ID, although you can come up with your own.
- Username and Password – You need to copy these two from the ‘Integrations’ page within your Mailtrap account.
- Need help? If so, feel free to refer to our Knowledge Base. 👀
- Email Feature – Here, you need to select the ‘Sending’ feature from the dropdown menu.
3. Create an email template
In the EmailJS dashboard, go to Email Templates and create a new template.
In the template editor, you’ll set up the email that will be sent out. This includes:
- Subject – For example, “New Contact Form Submission”
- Body content – You can write a message and use placeholders for form fields (e.g.,
{{name}}
,{{message}}
where you want to insert the sender’s name or message). - You can also configure advanced options like auto-reply, add attachments, etc., but as this is a demo, I’ll keep it basic.
Define the email’s content and placeholders for dynamic data. You might set up a template for a contact form email with placeholders like {{name}}
, {{email}}
, and {{message}}
where the user’s input will go. For example, you might create a template with an ID “contact_template” that has a body:
You have a new contact form message from {{name}} (email: {{email}}): “{{message}}” |
Note: EmailJS might show an option “Use default email address” for the sender – do NOT enable that. Instead, you should use the actual sender email (for Mailtrap, it should be from the domain you verified in Mailtrap). For example, if you verified yourdomain.com in Mailtrap, use something like noreply@yourdomain.com as the sender in your template.
Tip: If you include HTML in your template content, wrap placeholders with triple braces like {{{message}}}
to properly render HTML content.
Last but not least, once you’ve configured the service and template, be sure to note down your Service ID, Template ID, and your EmailJS Public Key. You can find these in your EmailJS dashboard. We will use them in the following chapters for our Replit app code to initialize EmailJS and send emails.
Create a React contact form
Now, let’s create a simple prompt for the Replit AI Agent:
Create a react form containing the following fields: “name”, “email”, “html_message”. “name” input is a string input “email” input is an html input “html_message” input is textarea, supporting any string including html code This should be a simple example form that should send emails (do not implement the actual email sending logic yet, just a placeholder for it) |
Important: Make sure the element name in the prompt matches the attachment’s name in the EmailJS dashboard. This is true for all sending cases throughout this article.
Next, if you input the prompt in the prompt field, Replit has a very useful Improve prompt feature. It allows you to refine your own prompts and make them more specific, ensuring the result meets your expectations:
After improving the prompt, we receive a more specific description of what we may need to look for:
A simple React form component for collecting user information and HTML message content with email sending capability. Initial form shouldn’t perform any actual actions on submit, use placeholder for it or mock the submit function. Core Features: – Text input field for user’s name (“name”) – Email input field for user’s email address (“email”) – HTML-enabled textarea for message content (“html_message”) – Form submission handler with email sending placeholder Visual References: Inspired by modern contact forms like those found on Medium and Ghost platforms, known for their clean and straightforward design. Style Guide: – Colors: Primary #007AFF (blue), Secondary #F5F5F5 (light grey), Text #333333 (dark grey), Error #FF3B30 (red), Success #34C759 (green) – Design: Inter/SF Pro Display fonts, single column layout, 16px padding, subtle shadows, responsive design with clear input validation states |
As soon as you’re ready, click Start building:
Note that one of the things that makes Replit different from other popular tools like Bolt.new or Lovable, is its step-by-step approach to creating web apps using AI Agents. As soon as it starts generating your project, it’ll provide a plan that you can review, check additional features that you may want to implement, approve, or even message the agent in case you have any questions.
For this demo, I didn’t go with any of the additional features, so we can simply click Approve plan & start.
The agent will immediately start generating a visual preview while the actual prototype of the application is being generated.
On the left, you will see a folder structure of the project generated by Replit AI agent. You can click ContactForm.tsx to view the code of the form generated by Replit. Since we didn’t provide any tool-specific requirements, Replit decided to use popular tools, such as TypeScript, Tailwind, Zod, and React Hook Form to simplify things.
I’ll use this default example further in the guide, but if you have any specific tool requirements, feel free to include them in your prompt. In the sidebar prompt field, you can ask Replit to regenerate the solution or create a new project by going back to the home page.
And here’s the ContactForm.tsx code snippet generated by Replit:
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useToast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import HtmlEditor from "./HtmlEditor";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Loader2 } from "lucide-react";
// Define the form schema with validation rules
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Please enter a valid email address"),
html_message: z.string().min(1, "Message is required"),
});
type FormValues = z.infer<typeof formSchema>;
export default function ContactForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast();
// Initialize form with react-hook-form and zod validation
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
email: "",
html_message: "",
},
});
// Mock form submission
const onSubmit = async (data: FormValues) => {
setIsSubmitting(true);
try {
// Simulate API call delay
await new Promise((resolve) => setTimeout(resolve, 1500));
// Show success toast
toast({
title: "Message sent!",
description: "We'll get back to you as soon as possible.",
variant: "success",
});
// Reset form
form.reset();
} catch (error) {
// Show error toast
toast({
title: "Something went wrong",
description: "Your message couldn't be sent. Please try again.",
variant: "destructive",
});
} finally {
setIsSubmitting(false);
}
};
return (
<div className="w-full max-w-md bg-white rounded-xl shadow-form p-8">
<div className="text-center mb-8">
<h1 className="text-2xl font-display font-semibold text-textColor">
Get in Touch
</h1>
<p className="text-gray-500 mt-2">We'd love to hear from you</p>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Name Field */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-gray-700">
Name
</FormLabel>
<FormControl>
<Input
placeholder="Your name"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors"
{...field}
/>
</FormControl>
<FormMessage className="text-destructive text-sm mt-1" />
</FormItem>
)}
/>
{/* Email Field */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-gray-700">
Email
</FormLabel>
<FormControl>
<Input
type="email"
placeholder="your.email@example.com"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors"
{...field}
/>
</FormControl>
<FormMessage className="text-destructive text-sm mt-1" />
</FormItem>
)}
/>
{/* HTML Message Field */}
<FormField
control={form.control}
name="html_message"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-gray-700">
Message
</FormLabel>
<FormControl>
<HtmlEditor
value={field.value}
onChange={field.onChange}
placeholder="Write your message here..."
/>
</FormControl>
<FormMessage className="text-destructive text-sm mt-1" />
</FormItem>
)}
/>
{/* Submit Button */}
<div className="pt-2">
<Button
type="submit"
className="w-full bg-primary hover:bg-primary/90 active:bg-primary/80 text-white font-medium py-2.5 px-4 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sending...
</>
) : (
"Send Message"
)}
</Button>
</div>
</form>
</Form>
</div>
);
}
And just like that, Replit has created a neat-looking form. However, it’s not fully interactable yet, which means we can’t send the actual emails. In the following steps, we’ll focus on adding an actual email-sending logic by using EmailJS, which we have configured previously.
Notes:
- Throughout the guide, I’ll primarily be using the Replit AI Agent. Just keep in mind that if you’re using the free version, it has a limitation of 10 user requests.
- If you want to perform simple specific changes, I recommend checking out the AI Assistant (Tools → Assistant). It’s better for minor questions and refactoring code than large app-scale updates that the Replit AI Agent provides you with.
Send email in React using EmailJS
In this step, we’ll modify the form to connect EmailJS email-sending logic on form submission. And while we can manually connect and configure EmailJS, using a new prompt would simplify things.
So, let’s start by creating a simple prompt in the sidebar prompt field:
Add EmailJS integration to the current form |
Since the AI Agent sidebar doesn’t support the Improve Prompt feature we mentioned in the previous chapter, we’ll rely on initial prompts:
After executing the prompt, Replit AI Agent will start updating the code to include the EmailJS email-sending logic. And, before actually writing any code, it will ask you about all the keys and IDs required to make it work. Remember those keys I mentioned you should note down in the EmailJS chapter? Now is the time to add them to the fields. 🙂
Note: In some cases, Replit may suggest adding a database to store user data. We won’t cover it in this article; however, feel free to use it in your app. For example, if your goal is a single-use contact form, you most likely don’t need it. On the other hand, if you plan to add authentication further on, where you would establish email communication with your users, you might consider adding a database to store user data. 💡
Moving on, let’s see if Replit put environment variables right there in the .env file, which should be located in the same folder as your index.html file (e.g. /client). If it’s not, move it.
VITE_EMAILJS_SERVICE_ID=${EMAILJS_SERVICE_ID}
VITE_EMAILJS_TEMPLATE_ID=${EMAILJS_TEMPLATE_ID}
VITE_EMAILJS_PUBLIC_KEY=${EMAILJS_PUBLIC_KEY}
If you’re wondering why you see ${VAR}
kind of values instead of the actual keys you entered, it’s because this is Replit’s approach to storing environment variables safely. They’re injected in the .env file when the app is running. You can find the actual keys by opening Tools → Secrets in the sidebar.
Next, let’s review the ContactForm.tsx changes. Go through your code; you should find the following two changes in your update form file:
- EmailJS is initialized.
// Initialize EmailJS
emailjs.init(import.meta.env.VITE_EMAILJS_PUBLIC_KEY);
- emailjs.send() is called in the onSubmit function.
const onSubmit = async (data: FormValues) => {
// Code before emailjs.send…
const response = await emailjs.send(
import.meta.env.VITE_EMAILJS_SERVICE_ID,
import.meta.env.VITE_EMAILJS_TEMPLATE_ID,
{
name: data.name,
email: data.email,
html_message: data.html_message,
}
);
// Code after emailjs.send…
}
With this setup, your form should be able to successfully send actual emails. If you fill out and submit the form, you should see a notification with the status of the email sending and a new email in Mailtrap Email Logs.
A quick note on sending plain-text emails
With the {{{message}}}
variable, the messages you enter are automatically formatted as HTML when we send them, and the variable can accept both HTML and simple messages.
However, if you want your emails to behave strictly as a ‘plain-text’, you can replace the triple brackets with double brackets {{message}}
in the EmailJS dashboard.
This applies to all of the examples below, so if you want to send plain-text emails, just update your placeholder value format to use double brackets in your EmailJS template.
Send HTML email
To send an HTML message, you can use the toolbar where you can choose multiple options, such as: bold, etc.
And here’s how’s our freshly-generated HTML looks in Mailtrap Email Logs:
Send an email with an attachment
Unfortunately, if you want to send attachments via EmailJS plan, you’ll need to subscribe to at least the ‘Personal Plan.’
Once you have subscribed, open the Attachments tab in your selected template. Then, click Add Attachment, select Variable Attachment, and enter the filename, content type (default), and parameter name that you’re going to use later in your HTML code. Click Apply and Save the template.
Now, we need a new prompt to add an input in the React form for file selection.
Add an ability to attach a file. The element should have a “my_file” name. The file should be passed via the template parameters as my_file in base64 format. |
Since Replit AI Agent will update the form to include the attachments field, let’s briefly go over the most important changes of the ContactForm.tsx:
- Added constant variables for accepted file sizes and file types:
// Max file size: 5MB
const MAX_FILE_SIZE = 5 * 1024 * 1024;
// Allowed file types
const ACCEPTED_FILE_TYPES = [
"application/pdf",
"image/jpeg",
"image/png",
"image/gif",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/plain",
];
- Added new states in the React component to handle file selection logic:
const [file, setFile] = useState<File | null>(null);
const [fileError, setFileError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
- Added a new function to handle adding of files:
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0] || null;
setFileError(null);
if (selectedFile) {
// Validate file size
if (selectedFile.size > MAX_FILE_SIZE) {
setFileError("File size exceeds 5MB limit");
setFile(null);
return;
}
// Validate file type
if (!ACCEPTED_FILE_TYPES.includes(selectedFile.type)) {
setFileError("File type not supported");
setFile(null);
return;
}
setFile(selectedFile);
} else {
setFile(null);
}
};
- Added a function to handle removal of files:
const removeFile = () => {
setFile(null);
setFileError(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
- Added a function to handle file conversion to base64 (compatible with EmailJS):
const convertFileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = error => reject(error);
});
};
- Updated the
onSubmit
handler to process files and convert them accordingly before sending.
const templateParams: Record<string, any> = {
name: data.name,
email: data.email,
html_message: data.html_message,
};
// If there's a file, convert it to base64 and add to template params
if (file) {
try {
const fileBase64 = await convertFileToBase64(file);
templateParams.my_file = fileBase64;
} catch (err) {
console.error("Error converting file:", err);
toast({
title: "File attachment error",
description: "There was an error processing your file. Please try again with a different file.",
variant: "destructive",
});
setIsSubmitting(false);
return;
}
}
- Updated the JSX to include the new file attachment input and related handling logic.
{/* File Attachment Field */}
<div className="space-y-2">
<Label
htmlFor="my_file"
className="text-sm font-medium text-gray-700"
>
Attachment
</Label>
<div className="flex items-center space-x-2">
<div className="relative flex-1">
<Input
id="my_file"
name="my_file"
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className={`w-full px-4 py-2 border border-gray-300 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-primary file:text-white hover:file:bg-primary/90 ${
fileError ? "border-destructive" : ""
}`}
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<Paperclip className="h-4 w-4 text-gray-400" />
</div>
</div>
{file && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={removeFile}
className="h-8 w-8 rounded-full bg-gray-100 hover:bg-gray-200"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{fileError && (
<p className="text-destructive text-sm mt-1">{fileError}</p>
)}
{file && (
<p className="text-sm text-gray-500">
File: {file.name} ({(file.size / 1024).toFixed(1)} KB)
</p>
)}
<p className="text-xs text-gray-500">
Max file size: 5MB. Supported formats: PDF, Images, Office documents, Text files
</p>
</div>
Finally, let’s add a new attachment using the new input and try to send an email:
You will notice the attachment data in the Raw tab in Email Logs. Meanwhile, the actual receiver will be able to view and download the attachment in their inbox.
Send email to multiple recipients
To send emails to multiple recipients, go to your template and add {{carbon_copy}}
in the Bcc field, just like so:
P.S. You can read more about carbon and blind carbon copies in our dedicated article. 👀
For the exemplary form, we’ll use a template value so that we can dynamically pass the recipients by mentioning them in the form. But you can also use static emails for the field, to send to some specific recipients.
Again, we’re starting with a new prompt:
Add an ability to mention multiple recipients using commas. The element should have a “carbon_copy” name and be a simple input. The same name for template parameters. |
After executing the prompt, you’ll notice a new CC Recipients
field being generated by Replit. Again, we’ll focus only on the most important changes of the ContactForm.tsx:
- Carbon copy included in the Zod schema (needed for correct validation during form submit):
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Please enter a valid email address"),
carbon_copy: z
.string()
.optional()
.refine(
(val) => {
if (!val) return true;
const emails = val.split(",").map((email) => email.trim());
return emails.every((email) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || email === ""
);
},
{
message: "Please enter valid email addresses separated by commas",
}
),
html_message: z.string().min(1, "Message is required"),
});
- Carbon copy included in template parameters in onSubmit handler:
const templateParams: Record<string, any> = {
name: data.name,
email: data.email,
carbon_copy: data.carbon_copy,
html_message: data.html_message,
};
- JSX code updated to include a new carbon copy input:
{/* Carbon Copy Field */}
<FormField
control={form.control}
name="carbon_copy"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-gray-700">
CC Recipients
</FormLabel>
<FormControl>
<Input
type="text"
id="carbon_copy"
name="carbon_copy"
placeholder="email1@example.com, email2@example.com"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors"
{...field}
/>
</FormControl>
<FormDescription className="text-xs text-gray-500 mt-1">
Separate multiple email addresses with commas
</FormDescription>
<FormMessage className="text-destructive text-sm mt-1" />
</FormItem>
)}
/>
And here’s how it should look in your form:
If everything was set correctly, several emails arrive in your Mailtrap Email Logs tab once you submit it.
Send email in React using EmailJS: security considerations
As a general rule, I don’t recommend exposing sensitive API keys in front-end code since it’s always accessible to users, meaning they can be extracted.
However, with EmailJS, the key is always exposed, and there’s pretty much no way around it. And since React is a client-side library, using any of the keys (e.g., Public Key) in the code will expose them.
Naturally, this raises the question of whether it’s safe to implement the method in your application. Technically, malicious users can script automated requests using your public key, leading to spam or excessive email usage.
So what’s the solution to all of this? Is it really not safe to use?
Luckily, EmailJS mitigates the issues to a satisfactory degree. Here are the highlights:
- Someone could copy your Public Key, but they’d only be able to send your predefined templates with your content. They wouldn’t be able to send custom emails with their own spam content, making it unappealing to spammers.
- EmailJS has multiple tools to prevent abuse, including IP-based rate limiting to block bot spam, an origin whitelist for added security, and optional reCAPTCHA tests to verify human interaction when sending emails.
Conclusion: The given methods make EmailJS safe enough to use in React for template-based email sending. However, you should still take note of the issues since your usage of EmailJS in React may vary and require additional security methods.
Here are additional official resources to help enhance your security:
Send emails using Mailtrap MCP
Mailtrap has recently launched its very own MCP server, and you can use it even in Replit. The integration workflow is similar to the official guide by Replit, meaning you can set it up in ~10 minutes.
Here’s how it works:
- Remix this template to get started:
- Open Tools → Secrets and in the Secrets tab, insert your own LLM API key.
- In Replit, you have to provide your own keys (e.g., OpenAI keys)
Important: Using custom LLM API keys often requires funding your account to support requests.
You can create and manage your OpenAI keys by creating an account in the platform accordingly.
- With your LLM API keys ready, open mcp-server-config.json and edit the file to include Mailtrap MCP configuration.
The updated configuration should include the new “mailtrap”
config in “mcpServers”
. Check it out:
{
"systemPrompt": "You are an AI assistant helping a software engineer",
"llm": {
"provider": "openai",
"model": "gpt-4o-mini",
"temperature": 0.2
},
"mcpServers": {
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"],
"requires_confirmation": ["fetch"]
},
"youtube": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/adhikasp/mcp-youtube",
"mcp-youtube"
]
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "./"]
},
"mailtrap": {
"command": "npx",
"args": ["-y", "mcp-mailtrap"],
"env": {
"MAILTRAP_API_TOKEN": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"DEFAULT_FROM_EMAIL": "no-reply@freelance.mailtrap.link"
}
}
}
}
Note: Make sure to replace MAILTRAP_API_TOKEN
and DEFAULT_FROM_EMAIL
placeholder values with your actual Mailtrap API credentials.
- Finally, open Tools → Shell and run the following command:
llm "Send a nicely formatted HTML email with greeting to john@example.com"
And, as usual, here’s the email in our Mailtrap Email Logs.
This demonstrates how Mailtrap MCP allows you to delegate email sending to the AI, including plain-text, HTML, and multiple-recipient emails. If you’re interested, we also have a few guides on integrating Mailtrap MCP with Claude or Cursor. Happy sending! 📨