ESP32 S3 Camera - Send Photo to Email When Motion Detected

This tutorial instructs you how to make an ESP32 S3 camera that sends you an email with a photo when somebody walks in front of it. A PIR motion sensor watches the room, the camera takes the picture, and the ESP32 S3 sends it straight to your mailbox. You do not need a Telegram account, a cloud service, or a public IP address.

ESP32 S3 Camera - Send Photo to Email When Motion Detected

What you'll build:

  1. An ESP32 S3 camera with a PIR motion sensor
  2. An email with the photo inside the message, every time the sensor sees a movement
  3. A quiet time, so one visitor does not fill your mailbox
  4. A camera that anybody can read, because everybody has an email address

Hardware Preparation

1×ESP32 S3 N16R8 OV5640 Camera
1×Alternatively, ESP32 S3 N16R8 OV2640 Camera
1×USB Cable Type-A to Type-C (for USB-A PC)
1×USB Cable Type-C to Type-C (for USB-C PC)
1×HC-SR501 Motion Sensor
1×Alternatively, AM312 Mini Motion Sensor
1×Jumper Wires
1×Optionally, DC Power Jack
1×Recommended: Screw Terminal Expansion Board for ESP32 S3
1×Recommended: Breakout Expansion Board for ESP32 S3
1×Recommended: Power Splitter for ESP32 S3

Or you can buy the following kits:

1×DIYables Sensor Kit (18 sensors/displays)
Disclosure: Some of the links provided in this section are Amazon affiliate links. We may receive a commission for any purchases made through these links at no additional cost to you.
Additionally, some of these links are for products from our own brand, DIYables .

※ NOTE THAT:

The ESP32 S3 camera board has 40 pins, but the screw terminal block and the breakout board have 44 pins. They still work together. Put the camera board in the center of the 44-pin board, so 2 pins stay free on the left side and 2 pins stay free on the right side.

The screw terminal block lets you connect wires with a screwdriver, with no soldering. The breakout board gives you easy pin headers for a breadboard.

Email or Telegram?

Both send the photo to your phone. They are good at different things:

EmailTelegram
Setup workMake one app passwordMake a bot and find a chat ID
Speed5 to 15 seconds2 to 4 seconds
Photo qualityFull size, shown inside the messageTelegram may make it smaller
Keeps old photosYes, your mailbox keeps everythingYes, but the chat gets long
Works for anybodyYes, everybody has an emailOnly people who use Telegram
Code sizeBigger, it needs a librarySmaller

Email is the better choice when you want the full size picture and a record you can search later. See also the ESP32 S3 Camera - Send Photo to Telegram When Motion Detected tutorial.

Overview of the App Password

Your normal Gmail password does not work here. Google blocks it for small devices. You must make a special 16-letter password, called an app password.

  • Make a new Gmail account only for your projects. Do not use the account you read your real email with.
  • Log into the new account.
  • Go to your Google Account.
  • Open the Security section.
Gmail security
  • Turn on 2-Step Verification. Without it, Google does not let you make an app password.
  • Open the Google App Passwords page and make a new app password. You can give it any name, for example ESP32 S3 Camera.
Google App Passwords
  • Click the Create button. Google shows 16 letters in four groups:
Gmail App Passwords
  • Keep these 16 letters. You need them in the code in the next step.
abcd efgh ijkl mnop

※ NOTE THAT:

Write the 16 letters without spaces into the code: abcdefghijklmnop

※ NOTE THAT:

Google changes the look of these pages from time to time. If you cannot find "App Passwords" with the steps above, search the internet for "How to get Google App Passwords" to see the newest way.

※ NOTE THAT:

An app password gives access to the whole mailbox of that account. This is why we use a new account only for projects. Keep the password secret, and never put it on a website or on GitHub. You can delete it at any time on the same Google page, and the ESP32 S3 simply stops sending.

Do you use another email company? Then change only these two lines:

CompanySMTP serverPort
Gmailsmtp.gmail.com465
Outlook / Hotmailsmtp.office365.com587
Yahoosmtp.mail.yahoo.com465

Overview of PIR Motion Sensor

A PIR motion sensor sees the heat of a human body. PIR means "Passive InfraRed". When a warm body moves in front of it, the OUT pin goes HIGH.

PIR Motion Sensor HC-SR501

Why This Project Uses a Sensor

The movement is found by the PIR sensor, not by the camera. This matters, because a PIR sensor answers only to the heat of a body. A person or an animal starts it. A lamp that goes on, a cloud that passes the sun, or a curtain that moves in the wind does not start it.

That is why a sensor sends you far fewer emails about nothing.

Way to find movementWhat starts itExtra partFalse alarms
PIR sensor (this tutorial)The heat of a bodyOne sensorAlmost none
The camera looking at itselfAny change in the pictureNoneLamps, clouds, wind, unless you work against them

Do you want to find movement with no sensor at all, using only the pictures of the camera? See the ESP32 S3 Camera - Detect Motion Without a Sensor tutorial. That way needs no extra part, and it also sees cold things such as a parcel or a car, which a PIR sensor never sees. The price is more code, and more work to ignore light and wind.

※ NOTE THAT:

Do you want the best of both? Use the sensor and the picture together, and send the email only when both agree. Then a warm body must be there and something must really have changed in the picture.

Key Specifications

ItemValue
Power5V
Output3.3V HIGH when it sees a movement, LOW when it sees nothing
Distance3 to 7 meters (you can change it)
Angleabout 110°
Warm-up time30 to 60 seconds after power on

The sensor has two small orange screws. Turn the time delay screw all the way to the left, so the output stays HIGH for only about 3 seconds.

For more detail about the motion sensor, see the ESP32 S3 - Motion Sensor tutorial.

Wiring Diagram between PIR Motion Sensor and ESP32 S3

Connect the PIR motion sensor to your ESP32 S3 with three jumper wires.

The wiring diagram between ESP32 S3 PIR Motion Sensor Email

This image is created using Fritzing. Click to enlarge image

PIR Sensor PinESP32 S3 Pin
VCC5V
GNDGND
OUTGPIO21

Safety Notes

The PIR sensor needs 5V on its VCC pin, not 3.3V. With 3.3V it looks like it works, but it gives wrong signals. Its OUT pin gives 3.3V, which is correct for the ESP32 S3.

The camera uses GPIO4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17 and 18. Do not use these pins for the sensor. GPIO21 is free, so we use it.

Install ESP Mail Client Library

The ESP Mail Client library talks to the email server for you.

  1. Open the Arduino IDE.
  2. Click the Libraries icon on the left bar.
  3. Type ESP Mail Client into the search box.
  4. Find the library by Mobizt and click INSTALL.
  • Search for ESP Mail Client created by Mobizt and click the Install button.
∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Library Manager
Type:
All
Topic:
All
ESP Mail Client by Mobizt
The library also supported other Arduino devices using Clients interfaces e.g. WiFiClient, EthernetClient, and GSMClient. More info
3.4.21
INSTALL
Newbiely.ino
···
1 void setup() {
Output
Serial Monitor
Ln 1, Col 1
ESP32S3 Dev Module on COM15
1

Arduino IDE Settings for ESP32 S3 Camera

The camera does not work with the default settings of the Arduino IDE. You must change some items in the Tools menu before you upload the code.

Tools Menu ItemSelect This
BoardESP32S3 Dev Module
Flash Size16MB (128Mb)
PSRAMOPI PSRAM
Partition SchemeHuge APP (3MB No OTA/1MB SPIFFS)
CPU Frequency240MHz (WiFi)
USB ModeHardware CDC and JTAG
Upload ModeUART0 / Hardware CDC
Upload Speed921600
USB CDC On BootEnabled

※ NOTE THAT:

Partition Scheme must be Huge APP for this project. The email library is big, and the code does not fit in the normal partition.

How To Send a Photo by Email

  • Include the library at the top of your code.
#include <ESP_Mail_Client.h> SMTPSession smtp;
  • Put your email account into a Session_Config.
Session_Config config; config.server.host_name = "smtp.gmail.com"; config.server.port = 465; config.login.email = SENDER_EMAIL; config.login.password = SENDER_PASSWORD; // the 16-letter app password

※ NOTE THAT:

The email server also needs the correct time, or it refuses the message. The code asks an NTP time server for you with config.time.ntp_server.

  • Write the message.
SMTP_Message message; message.sender.name = "ESP32 S3 Camera"; message.sender.email = SENDER_EMAIL; message.subject = "Motion detected!"; message.addRecipient("Me", RECEIVER_EMAIL); // Plain text, for old email programs that show no pictures message.text.content = "The camera saw a movement.";

Inside the Email, Not Under It

There are two ways to put a photo into an email, and they look very different when the message arrives:

WayMethod in the codeHow it looks
Attachmentmessage.addAttachment(att);A small grey file box under the text. You must click it to see the photo.
Inside the emailmessage.addInlineImage(att);The photo is big, in the message itself. You see it at once.

For a security camera, inside the email is much better. You open the message on your phone and you see the visitor immediately, with no tapping.

Two things are needed for it:

  • The body of the email must be HTML, not plain text
  • The <img src="..."> name must match the file name of the picture
Photo Inside the Email

※ NOTE THAT:

Do you want both, so you can also save the file easily? Then add the same picture two times, once with addInlineImage() and once with addAttachment().

  • Write the body of the email in HTML, and put an <img> in it.
message.html.content = "<div style=\"font-family:sans-serif\">" "<h2>Motion detected!</h2>" "<p>The camera saw a movement. This is what it looked like:</p>" "<img src=\"photo.jpg\" style=\"width:100%;max-width:640px\">" "</div>"; message.html.charSet = "utf-8";
  • Put the photo inside the email with addInlineImage(). The photo goes straight from the memory, so it never has to be saved to a card first.
SMTP_Attachment att; att.descr.filename = "photo.jpg"; att.descr.mime = "image/jpeg"; att.descr.transfer_encoding = Content_Transfer_Encoding::enc_base64; att.blob.data = fb->buf; att.blob.size = fb->len; message.addInlineImage(att);

※ NOTE THAT:

The name in src="photo.jpg" must be exactly the same as att.descr.filename. That is how the email program knows which picture belongs in which place. Change one of them and you get an empty box instead of the photo.

※ NOTE THAT:

blob means "the data is already in the memory". The camera gives you fb->buf and fb->len, and the library sends exactly those bytes.

  • Connect and send.
if (!smtp.connect(&config)) { Serial.printf("Cannot connect. Reason: %s\n", smtp.errorReason().c_str()); return false; } MailClient.sendMail(&smtp, &message); smtp.closeSession();

※ NOTE THAT:

Always call smtp.closeSession(). If you leave the connection open, the email server soon refuses new messages.

ESP32 S3 Code

Change your WiFi, your email address and your app password at the top of the code. The quiet time is 60 seconds, because an email takes longer than a chat message. To change it, change the line #define QUIET_TIME 60000.

/* * This ESP32 S3 code was developed by newbiely.com * * This ESP32 S3 code is made available for public use without any restriction * * For comprehensive instructions and wiring diagrams, please visit: * https://newbiely.com/tutorials/esp32-s3/esp32-s3-camera-send-photo-to-email-when-motion-detected */ #include <WiFi.h> #include <ESP_Mail_Client.h> #include "esp_camera.h" const char *WIFI_SSID = "YOUR_WIFI_SSID"; // CHANGE IT const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; // CHANGE IT #define SMTP_HOST "smtp.gmail.com" #define SMTP_PORT 465 #define SENDER_EMAIL "your_email@gmail.com" // CHANGE IT #define SENDER_PASSWORD "your_16_letter_app_password" // CHANGE IT #define RECEIVER_EMAIL "your_email@gmail.com" // CHANGE IT #define PIR_PIN 21 // the PIR motion sensor is connected to GPIO21 #define QUIET_TIME 60000 // wait 60 seconds before the next email (60000 milliseconds) // Camera pins of the ESP32-S3-N16R8 camera board. // The same pins work for the OV2640, OV3660 and OV5640 versions. #define PWDN_GPIO_NUM -1 #define RESET_GPIO_NUM -1 #define XCLK_GPIO_NUM 15 #define SIOD_GPIO_NUM 4 #define SIOC_GPIO_NUM 5 #define Y9_GPIO_NUM 16 #define Y8_GPIO_NUM 17 #define Y7_GPIO_NUM 18 #define Y6_GPIO_NUM 12 #define Y5_GPIO_NUM 10 #define Y4_GPIO_NUM 8 #define Y3_GPIO_NUM 9 #define Y2_GPIO_NUM 11 #define VSYNC_GPIO_NUM 6 #define HREF_GPIO_NUM 7 #define PCLK_GPIO_NUM 13 SMTPSession smtp; int lastPirState = LOW; unsigned long lastSendTime = 0; // Returns the name of the camera sensor that is installed on the board const char *getSensorName(int pid) { switch (pid) { case OV2640_PID: return "OV2640 (2 MP, max UXGA 1600x1200)"; case OV3660_PID: return "OV3660 (3 MP, max QXGA 2048x1536)"; case OV5640_PID: return "OV5640 (5 MP, max QSXGA 2592x1944)"; default: return "Unknown sensor"; } } bool initCamera() { camera_config_t config; config.ledc_channel = LEDC_CHANNEL_0; config.ledc_timer = LEDC_TIMER_0; config.pin_d0 = Y2_GPIO_NUM; config.pin_d1 = Y3_GPIO_NUM; config.pin_d2 = Y4_GPIO_NUM; config.pin_d3 = Y5_GPIO_NUM; config.pin_d4 = Y6_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM; config.pin_d6 = Y8_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM; config.pin_xclk = XCLK_GPIO_NUM; config.pin_pclk = PCLK_GPIO_NUM; config.pin_vsync = VSYNC_GPIO_NUM; config.pin_href = HREF_GPIO_NUM; config.pin_sccb_sda = SIOD_GPIO_NUM; config.pin_sccb_scl = SIOC_GPIO_NUM; config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG; config.frame_size = FRAMESIZE_SVGA; // 800x600 - small, so the email is sent quickly config.jpeg_quality = 12; // 0 to 63, a lower number means a better image config.fb_count = 1; config.fb_location = CAMERA_FB_IN_DRAM; config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; if (psramFound()) { config.fb_location = CAMERA_FB_IN_PSRAM; config.jpeg_quality = 10; config.fb_count = 2; config.grab_mode = CAMERA_GRAB_LATEST; } else { config.frame_size = FRAMESIZE_QVGA; Serial.println("WARNING: PSRAM not found. Please set PSRAM to OPI PSRAM in the Tools menu."); } esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x\n", err); return false; } sensor_t *s = esp_camera_sensor_get(); Serial.printf("Camera sensor: %s\n", getSensorName(s->id.PID)); // Each sensor needs slightly different settings switch (s->id.PID) { case OV2640_PID: s->set_vflip(s, 0); s->set_hmirror(s, 0); break; case OV3660_PID: s->set_vflip(s, 1); // the OV3660 image is upside down by default s->set_brightness(s, 1); // make the image a little brighter s->set_saturation(s, -2); // make the colors a little softer break; case OV5640_PID: s->set_vflip(s, 0); s->set_hmirror(s, 0); break; default: break; } return true; } // Sends one photo as an email attachment bool sendPhotoByEmail(camera_fb_t *fb) { Session_Config config; config.server.host_name = SMTP_HOST; config.server.port = SMTP_PORT; config.login.email = SENDER_EMAIL; config.login.password = SENDER_PASSWORD; config.login.user_domain = ""; // The email server needs the correct time, so we ask an NTP server config.time.ntp_server = "pool.ntp.org,time.nist.gov"; config.time.gmt_offset = 0; config.time.day_light_offset = 0; SMTP_Message message; message.sender.name = "ESP32 S3 Camera"; message.sender.email = SENDER_EMAIL; message.subject = "Motion detected!"; message.addRecipient("Me", RECEIVER_EMAIL); // Plain text, for old email programs that show no pictures message.text.content = "The camera saw a movement. Open this email in a program that shows pictures."; message.text.charSet = "utf-8"; // The email body. The name inside src="..." must be the same as the // file name of the picture below. That is how the two find each other. message.html.content = "<div style=\"font-family:sans-serif\">" "<h2>Motion detected!</h2>" "<p>The camera saw a movement. This is what it looked like:</p>" "<img src=\"photo.jpg\" style=\"width:100%;max-width:640px\">" "</div>"; message.html.charSet = "utf-8"; // The photo goes INSIDE the email, not under it as a small file. // addInlineImage() is what makes the difference, not addAttachment(). SMTP_Attachment att; att.descr.filename = "photo.jpg"; att.descr.mime = "image/jpeg"; att.descr.transfer_encoding = Content_Transfer_Encoding::enc_base64; att.blob.data = fb->buf; att.blob.size = fb->len; message.addInlineImage(att); Serial.println("Connecting to the email server..."); if (!smtp.connect(&config)) { Serial.printf("Cannot connect. Reason: %s\n", smtp.errorReason().c_str()); return false; } if (!MailClient.sendMail(&smtp, &message)) { Serial.printf("Cannot send the email. Reason: %s\n", smtp.errorReason().c_str()); smtp.closeSession(); return false; } smtp.closeSession(); Serial.println("The email was sent"); return true; } void takeAndSendPhoto() { camera_fb_t *fb = esp_camera_fb_get(); if (!fb) { Serial.println("Photo capture failed"); return; } Serial.printf("Photo taken: %u bytes\n", fb->len); sendPhotoByEmail(fb); esp_camera_fb_return(fb); // give the memory back to the camera } void setup() { Serial.begin(115200); delay(1000); pinMode(PIR_PIN, INPUT); if (!initCamera()) { Serial.println("Stopped"); while (true) delay(1000); } WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Connected. IP address: "); Serial.println(WiFi.localIP()); MailClient.networkReconnect(true); smtp.debug(0); // change to 1 to see every step of the email server Serial.println("The PIR sensor is warming up. Please wait 30 seconds and do not move."); delay(30000); Serial.println("Ready. Waiting for motion..."); } void loop() { int pirState = digitalRead(PIR_PIN); // LOW to HIGH means the sensor has just seen a movement if (lastPirState == LOW && pirState == HIGH) { if (millis() - lastSendTime >= QUIET_TIME) { Serial.println("Motion detected!"); takeAndSendPhoto(); lastSendTime = millis(); } } lastPirState = pirState; }

Detailed Instructions

  • New to ESP32 S3? Complete our Getting Started with ESP32 S3 guide first.
  • Make your Google app password. See the steps above.
  • Install the ESP Mail Client library.
  • Unplug the USB cable, then wire the PIR sensor as in the table above.
  • Turn the time delay screw of the sensor all the way to the left.
  • Copy the above code and paste it into the Arduino IDE.
  • Change WIFI_SSID, WIFI_PASSWORD, SENDER_EMAIL, SENDER_PASSWORD and RECEIVER_EMAIL.

※ NOTE THAT:

  • SENDER_EMAIL must be a Gmail account, the new one you made for your projects.
  • SENDER_PASSWORD is the 16-letter app password, not the password of the account.
  • RECEIVER_EMAIL can be any kind of email address. It can also be the same address, so the camera writes to itself.
  • Change the settings in the Tools menu. See the table above.
  • Compile and upload the code to the ESP32 S3 board by clicking the Upload button in Arduino IDE.
Arduino IDE Upload Code
  • Open the Serial Monitor in Arduino IDE.
How to open serial monitor on Arduino IDE
  • Press the RESET button one time, then leave the room for 30 seconds.
  • Come back and walk in front of the camera. The email arrives in a few seconds.
ESP32 S3 Camera Photo in Email

Serial Monitor

The following readings were captured in 2026:

∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'ESP32S3 Dev Module' on 'COM15')
New Line
9600 baud
[2026-09-28 10:00:01] Camera sensor: OV5640 (5 MP, max QSXGA 2592x1944) [2026-09-28 10:00:01] Connecting to WiFi... [2026-09-28 10:00:04] Connected. IP address: 192.168.0.2 [2026-09-28 10:00:04] The PIR sensor is warming up. Please wait 30 seconds and do not move. [2026-09-28 10:00:34] Ready. Waiting for motion... [2026-09-28 10:01:12] Motion detected! [2026-09-28 10:01:12] Photo taken: 51353 bytes [2026-09-28 10:01:12] Connecting to the email server... [2026-09-28 10:01:20] The email was sent
Ln 11, Col 1
ESP32S3 Dev Module on COM15
2

※ NOTE THAT:

Do you want to see every step that the email server does? Change smtp.debug(0) to smtp.debug(1). The Serial Monitor then shows the whole conversation, which helps a lot when something goes wrong.

Troubleshooting

The Serial Monitor shows Cannot connect with Username and Password not accepted.

You used your normal Gmail password. Make an app password and use that one, with no spaces.

The Serial Monitor shows a certificate or a time error.

The board has the wrong time, so it cannot check the server. Make sure the board reaches the internet, because the time comes from an NTP server.

The email arrives, but the photo is an empty box.

The name in src="photo.jpg" and the name in att.descr.filename are not the same. They must match exactly.

The photo is still a small file under the text.

You used addAttachment(). Change it to addInlineImage(), and make sure the body is message.html.content, not only message.text.content.

The email never arrives, but the Serial Monitor says it was sent.

Look in the Spam folder. The first message from a new sender often lands there. Mark it as "not spam" one time, and the next ones go to your inbox.

The Serial Monitor shows Connection error with Outlook.

Outlook uses port 587, not 465. Change SMTP_PORT to 587.

The upload fails with "text section exceeds available space".

Set Partition Scheme to Huge APP in the Tools menu. The email library is big.

The sensor sees a movement all the time.

Wait one full minute after start, turn the sensitivity screw a little to the left, and check that VCC is on 5V.

I get too many emails.

Increase QUIET_TIME from 60000 to 300000 (five minutes).

The Serial Monitor shows nothing after the boot messages.

If your USB cable is in the port named UART, set USB CDC On Boot to Disabled. If it is in the port named USB, set it to Enabled.

Applications

Email needs no special application, so anybody can receive the photos, on any phone and on any computer. Here are practical projects you can build with this code:

  1. Home security camera with email alerts: Get a full size photo in your mailbox when somebody enters.
  2. Package delivery record: Keep a dated email for every delivery at your door.
  3. Office after hours: Send the photos to a shared company mailbox that several people can read.
  4. Holiday house watcher: Watch an empty house, and keep every photo as a record.
  5. Wildlife camera with reports: Get an email when animals come into your garden at night.
  6. Farm gate and barn: Watch a place far from your house, and keep the emails as proof.
  7. Shop alarm record: Every photo has a date and a time, so it works as a simple log book.
  8. Baby room watcher: Get a photo when your child gets out of bed.
  9. Tutorial and machine room: Know who entered the room while the machines were running.
  10. Insurance proof: A mailbox full of dated photos is easy to search and easy to show later.

Video Tutorial

Watch the step-by-step video walkthrough for this ESP32 S3 project below.

Challenges

  1. Beginner: Change the subject and the text of the email to your own words.
  2. Intermediate: Send the email to two people at the same time with a second message.addRecipient().
  3. Advanced: Also save every photo to the microSD card, so you keep a copy even when the internet is down. See the ESP32 S3 Camera - Save Photo to SD Card When Motion Detected tutorial.

※ OUR MESSAGES

  • As freelancers, We are AVAILABLE for HIRE. See how to outsource your project to us
  • Please feel free to share the link of this tutorial. However, Please do not use our content on any other websites. We invested a lot of effort and time to create the content, please respect our work!