Basic Email Sender
Abstract
Email is older than the web and still drives most automated notifications, password resets, marketing flows, and alerts. In this project you will build a Tkinter GUI that sends real email via Gmail SMTP. We start with a plain-text version, then upgrade to HTML bodies, multiple recipients, file attachments, retries, secrets management, and end with a sketch of a transactional-email service. Along the way you learn why Gmail wants app passwords instead of your real one, and how smtplibsmtplib lays out an email under the hood.
You will learn:
- The shape of SMTP — connect, STARTTLS, login, MAIL FROM, RCPT TO, DATA, QUIT.
- Why Gmail blocks plain-password logins and how App Passwords fix it.
- How
email.mime.*email.mime.*builds multipart messages. - How to validate input safely and handle network errors.
- How to manage secrets so credentials never sit in source control.
Prerequisites
- Python 3.6 or above.
- A text editor or IDE.
- A Gmail account with 2-Step Verification enabled.
- An App Password generated at myaccount.google.com/apppasswords.
Set Up Gmail App Password
- Visit myaccount.google.com/security.
- Enable 2-Step Verification (required to access App Passwords).
- Go to App passwords → generate a new one named “Python Mailer”.
- Copy the 16-character code. This replaces your normal password in scripts.
- Treat it like a password. Anyone with this string can send mail as you.
Getting Started
Create the project
- Create folder
email-senderemail-sender. - Inside, create
emailsender.pyemailsender.py.
Write the code
Email Sender
Source# Basic Email Sender in Python
# Before the using your gmail and password follows these steps:
# 1. Go to your google account
# 2. Click on Security
# 3. Under "Signing in to Google," select 2-Step Verification.
# 4. At the bottom of the page, select App passwords.
# 5. Enter a name that helps you remember where you’ll use the app password.
# 6. Select Generate.
# 7. To enter the app password, follow the instructions on your screen. The app password is the 16-character code that generates on your device.
# 8. Select Done.
# 9. Use Generated Password in the password field
# For More Infomation: https://support.google.com/mail/?p=InvalidSecondFactor
import tkinter as tk # pip install tk
from tkinter import *
import smtplib # pip install smtplib
from tkinter import messagebox
import re
def sendEmail():
try:
sender = email.get()
rec = receiver.get()
pas = password.get()
msg = message.get()
# Validating the Email
if(sender == "Enter Your Email" or rec == "Enter Receiver's Email" or pas == "Enter Your Password" or msg == "Enter Your Message"):
messagebox.showerror("Error", "Please Enter All The Fields")
return
# Validate Email Using Regular Expression
email_regex = re.compile(r"[^@]+@[^@]+\.[^@]+")
if not email_regex.match(sender):
messagebox.showerror("Error", "Invalid Email")
return
if not email_regex.match(rec):
messagebox.showerror("Error", "Invalid Receiver's Email")
return
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()
server.login(sender, pas)
server.sendmail(sender, rec, msg)
server.quit()
messagebox.showinfo("Success", "Email Sent Successfully")
email.delete(0, END)
receiver.delete(0, END)
password.delete(0, END)
message.delete(0, END)
email.insert(0, "Enter Your Email")
receiver.insert(0, "Enter Receiver's Email")
password.insert(0, "Enter Your Password")
message.insert(0, "Enter Your Message")
except Exception as e:
messagebox.showerror("Error", "Something Went Wrong")
print(e)
root = tk.Tk()
root.title("Email Sender")
root.geometry("700x700")
root.config(background="white")
label_file_explorer = Label(root, text = "Email Sender using Tkinter", width = 100, height = 3, fg = "gray", bg = "whitesmoke")
email = Entry(root, width = 50)
email.insert(0, "Enter Your Email")
receiver = Entry(root, width = 50)
receiver.insert(0, "Enter Receiver's Email")
password = Entry(root, width = 50)
password.insert(0, "Enter Your Password")
message = Entry(root, width = 50)
message.insert(0, "Enter Your Message")
button_explore = Button(root, text = "Send Email", command = sendEmail)
exit_button = Button(root, text = "Exit", command = root.destroy)
label_file_explorer.grid(column = 1, row = 1)
email.grid(column = 1, row = 2)
receiver.grid(column = 1, row = 3)
password.grid(column = 1, row = 4)
message.grid(column = 1, row = 5)
button_explore.grid(column = 1, row = 6)
exit_button.grid(column = 1, row = 7)
root.mainloop()
# Basic Email Sender in Python
# Before the using your gmail and password follows these steps:
# 1. Go to your google account
# 2. Click on Security
# 3. Under "Signing in to Google," select 2-Step Verification.
# 4. At the bottom of the page, select App passwords.
# 5. Enter a name that helps you remember where you’ll use the app password.
# 6. Select Generate.
# 7. To enter the app password, follow the instructions on your screen. The app password is the 16-character code that generates on your device.
# 8. Select Done.
# 9. Use Generated Password in the password field
# For More Infomation: https://support.google.com/mail/?p=InvalidSecondFactor
import tkinter as tk # pip install tk
from tkinter import *
import smtplib # pip install smtplib
from tkinter import messagebox
import re
def sendEmail():
try:
sender = email.get()
rec = receiver.get()
pas = password.get()
msg = message.get()
# Validating the Email
if(sender == "Enter Your Email" or rec == "Enter Receiver's Email" or pas == "Enter Your Password" or msg == "Enter Your Message"):
messagebox.showerror("Error", "Please Enter All The Fields")
return
# Validate Email Using Regular Expression
email_regex = re.compile(r"[^@]+@[^@]+\.[^@]+")
if not email_regex.match(sender):
messagebox.showerror("Error", "Invalid Email")
return
if not email_regex.match(rec):
messagebox.showerror("Error", "Invalid Receiver's Email")
return
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()
server.login(sender, pas)
server.sendmail(sender, rec, msg)
server.quit()
messagebox.showinfo("Success", "Email Sent Successfully")
email.delete(0, END)
receiver.delete(0, END)
password.delete(0, END)
message.delete(0, END)
email.insert(0, "Enter Your Email")
receiver.insert(0, "Enter Receiver's Email")
password.insert(0, "Enter Your Password")
message.insert(0, "Enter Your Message")
except Exception as e:
messagebox.showerror("Error", "Something Went Wrong")
print(e)
root = tk.Tk()
root.title("Email Sender")
root.geometry("700x700")
root.config(background="white")
label_file_explorer = Label(root, text = "Email Sender using Tkinter", width = 100, height = 3, fg = "gray", bg = "whitesmoke")
email = Entry(root, width = 50)
email.insert(0, "Enter Your Email")
receiver = Entry(root, width = 50)
receiver.insert(0, "Enter Receiver's Email")
password = Entry(root, width = 50)
password.insert(0, "Enter Your Password")
message = Entry(root, width = 50)
message.insert(0, "Enter Your Message")
button_explore = Button(root, text = "Send Email", command = sendEmail)
exit_button = Button(root, text = "Exit", command = root.destroy)
label_file_explorer.grid(column = 1, row = 1)
email.grid(column = 1, row = 2)
receiver.grid(column = 1, row = 3)
password.grid(column = 1, row = 4)
message.grid(column = 1, row = 5)
button_explore.grid(column = 1, row = 6)
exit_button.grid(column = 1, row = 7)
root.mainloop()
Run it
C:\Users\Your Name\email-sender> python emailsender.py
# GUI opens with sender / receiver / password / message fields and Send buttonC:\Users\Your Name\email-sender> python emailsender.py
# GUI opens with sender / receiver / password / message fields and Send buttonSMTP in Seven Lines
The core is shorter than the GUI around it:
import smtplib
with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.starttls()
server.login("you@gmail.com", "16-char-app-password")
server.sendmail("you@gmail.com", "friend@example.com",
"Subject: Hello\n\nThis is the body.")import smtplib
with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.starttls()
server.login("you@gmail.com", "16-char-app-password")
server.sendmail("you@gmail.com", "friend@example.com",
"Subject: Hello\n\nThis is the body.")Step by step:
- Connect to Gmail’s SMTP on port 587.
STARTTLSSTARTTLSupgrades the connection to encrypted.- Login with your address + App Password.
sendmailsendmaildelivers the message.- The
withwithblock automatically issuesQUITQUITon exit.
The Subject:Subject: header is required for Gmail to show a subject line; everything before the blank line is headers, everything after is the body.
Step-by-Step (GUI Version)
1. Imports
import tkinter as tk
from tkinter import messagebox
import smtplib, re
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipartimport tkinter as tk
from tkinter import messagebox
import smtplib, re
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart2. Build the form
root = tk.Tk()
root.title("Email Sender")
root.geometry("520x420")
sender_entry = tk.Entry(root, width=50); sender_entry.pack(pady=5)
receiver_entry = tk.Entry(root, width=50); receiver_entry.pack(pady=5)
password_entry = tk.Entry(root, width=50, show="*"); password_entry.pack(pady=5)
subject_entry = tk.Entry(root, width=50); subject_entry.pack(pady=5)
message_text = tk.Text(root, width=50, height=10); message_text.pack(pady=5)root = tk.Tk()
root.title("Email Sender")
root.geometry("520x420")
sender_entry = tk.Entry(root, width=50); sender_entry.pack(pady=5)
receiver_entry = tk.Entry(root, width=50); receiver_entry.pack(pady=5)
password_entry = tk.Entry(root, width=50, show="*"); password_entry.pack(pady=5)
subject_entry = tk.Entry(root, width=50); subject_entry.pack(pady=5)
message_text = tk.Text(root, width=50, height=10); message_text.pack(pady=5)show="*"show="*" masks the password as the user types — a small but expected UX detail.
3. Validate and send
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
def send():
sender = sender_entry.get().strip()
receivers = [r.strip() for r in receiver_entry.get().split(",") if r.strip()]
pwd = password_entry.get()
subject = subject_entry.get().strip() or "(no subject)"
body = message_text.get("1.0", "end").strip()
if not EMAIL_RE.match(sender):
return messagebox.showerror("Error", "Invalid sender address.")
for r in receivers:
if not EMAIL_RE.match(r):
return messagebox.showerror("Error", f"Invalid recipient: {r}")
if not pwd or not body:
return messagebox.showerror("Error", "Password and body required.")
msg = MIMEMultipart()
msg["From"] = sender
msg["To"] = ", ".join(receivers)
msg["Subject"] = subject
msg.attach(MIMEText(body, "plain"))
try:
with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.starttls()
server.login(sender, pwd)
server.send_message(msg, from_addr=sender, to_addrs=receivers)
except smtplib.SMTPAuthenticationError:
return messagebox.showerror("Auth failed", "Wrong password? Use an App Password.")
except smtplib.SMTPException as e:
return messagebox.showerror("SMTP error", str(e))
except Exception as e:
return messagebox.showerror("Network error", str(e))
messagebox.showinfo("Sent", "Email sent successfully.")EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
def send():
sender = sender_entry.get().strip()
receivers = [r.strip() for r in receiver_entry.get().split(",") if r.strip()]
pwd = password_entry.get()
subject = subject_entry.get().strip() or "(no subject)"
body = message_text.get("1.0", "end").strip()
if not EMAIL_RE.match(sender):
return messagebox.showerror("Error", "Invalid sender address.")
for r in receivers:
if not EMAIL_RE.match(r):
return messagebox.showerror("Error", f"Invalid recipient: {r}")
if not pwd or not body:
return messagebox.showerror("Error", "Password and body required.")
msg = MIMEMultipart()
msg["From"] = sender
msg["To"] = ", ".join(receivers)
msg["Subject"] = subject
msg.attach(MIMEText(body, "plain"))
try:
with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.starttls()
server.login(sender, pwd)
server.send_message(msg, from_addr=sender, to_addrs=receivers)
except smtplib.SMTPAuthenticationError:
return messagebox.showerror("Auth failed", "Wrong password? Use an App Password.")
except smtplib.SMTPException as e:
return messagebox.showerror("SMTP error", str(e))
except Exception as e:
return messagebox.showerror("Network error", str(e))
messagebox.showinfo("Sent", "Email sent successfully.")MIMEMultipartMIMEMultipart is the foundation for attachments and HTML; even for plain text it costs almost nothing to use.
HTML Emails
To send rich content:
html = """<h2>Hello!</h2><p>This is <b>HTML</b> email.</p>
<p><a href="https://example.com">Click here</a></p>"""
msg = MIMEMultipart("alternative")
msg.attach(MIMEText("Plain-text fallback", "plain"))
msg.attach(MIMEText(html, "html"))html = """<h2>Hello!</h2><p>This is <b>HTML</b> email.</p>
<p><a href="https://example.com">Click here</a></p>"""
msg = MIMEMultipart("alternative")
msg.attach(MIMEText("Plain-text fallback", "plain"))
msg.attach(MIMEText(html, "html"))The "alternative""alternative" multipart type tells clients to show one of the parts — modern clients use HTML, plain-text-only clients fall back to the text part.
Attachments
from email.mime.base import MIMEBase
from email import encoders
def attach_file(msg, path):
with open(path, "rb") as f:
part = MIMEBase("application", "octet-stream")
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header("Content-Disposition",
f'attachment; filename="{os.path.basename(path)}"')
msg.attach(part)
attach_file(msg, "report.pdf")
attach_file(msg, "screenshot.png")from email.mime.base import MIMEBase
from email import encoders
def attach_file(msg, path):
with open(path, "rb") as f:
part = MIMEBase("application", "octet-stream")
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header("Content-Disposition",
f'attachment; filename="{os.path.basename(path)}"')
msg.attach(part)
attach_file(msg, "report.pdf")
attach_file(msg, "screenshot.png")Files become base64-encoded MIME parts. Most providers cap attachments around 25 MB.
Secret Management
Hard-coding the App Password into emailsender.pyemailsender.py is the most common security mistake in this project. Better:
import os
sender = os.environ["EMAIL_USER"]
password = os.environ["EMAIL_PASSWORD"]import os
sender = os.environ["EMAIL_USER"]
password = os.environ["EMAIL_PASSWORD"]Set them once:
export EMAIL_USER=you@gmail.com
export EMAIL_PASSWORD=xxxx-xxxx-xxxx-xxxxexport EMAIL_USER=you@gmail.com
export EMAIL_PASSWORD=xxxx-xxxx-xxxx-xxxxOr use python-dotenvpython-dotenv to read from a .env.env file (add .env.env to .gitignore.gitignore).
For desktop apps, the OS keychain via keyringkeyring is the right answer:
import keyring
keyring.set_password("PCH-mailer", sender, app_password) # one-time setup
pwd = keyring.get_password("PCH-mailer", sender) # laterimport keyring
keyring.set_password("PCH-mailer", sender, app_password) # one-time setup
pwd = keyring.get_password("PCH-mailer", sender) # laterCommon Mistakes
| Problem | Cause | Fix |
|---|---|---|
| “Username and Password not accepted” | Used real Gmail password | Generate an App Password |
| Mail goes to Spam | No proper FromFrom, no DKIM, plain-text body | Use SendGrid/Mailgun for production |
ConnectionRefusedErrorConnectionRefusedError | Wrong port | 587 for STARTTLS, 465 for smtplib.SMTP_SSLsmtplib.SMTP_SSL |
| Subject missing | Did not add the header | Set msg["Subject"]msg["Subject"] |
| Emoji breaks subject | Default encoding mismatch | Use MIMEText(body, _charset="utf-8")MIMEText(body, _charset="utf-8") |
| Multiple recipients only first receives | Passed string, not list | Split on commas and pass a list |
Variations to Try
1. CC and BCC
msg["Cc"] = "manager@example.com"
all_recipients = receivers + ["manager@example.com", "hidden@example.com"]
server.send_message(msg, from_addr=sender, to_addrs=all_recipients)msg["Cc"] = "manager@example.com"
all_recipients = receivers + ["manager@example.com", "hidden@example.com"]
server.send_message(msg, from_addr=sender, to_addrs=all_recipients)BCC is just a recipient with no header — pass to send_messagesend_message’s to_addrsto_addrs but don’t add a header.
2. Personalized mail merge
for row in csv.DictReader(open("contacts.csv")):
body = template.format(**row)
send(row["email"], body)for row in csv.DictReader(open("contacts.csv")):
body = template.format(**row)
send(row["email"], body)Read CSV → format template per row → send.
3. Other providers
- Outlook:
smtp-mail.outlook.com:587smtp-mail.outlook.com:587. - Yahoo:
smtp.mail.yahoo.com:587smtp.mail.yahoo.com:587(also app passwords). - iCloud:
smtp.mail.me.com:587smtp.mail.me.com:587.
4. Inline images
Use MIMEImageMIMEImage and reference with <img src="cid:logo"><img src="cid:logo">. The cid:cid: matches a Content-IDContent-ID header on the image part.
5. Scheduled send
Combine with Simple Reminder App — fire send()send() at a specific time.
6. Bounce handling
Read the SMTP response. server.send_messageserver.send_message returns a dict of failed recipients.
7. Production-grade with SendGrid / Mailgun
For more than a few emails per day, switch to a transactional-email provider. They handle deliverability, SPF/DKIM signing, bounce processing, and analytics.
8. CLI mode with argparseargparse
mailer.py --to friend@example.com --subject Hi --body "Hello!" --attach report.pdfmailer.py --to friend@example.com --subject Hi --body "Hello!" --attach report.pdf9. Email server preview with MailHog
For development: run mailhogmailhog locally, point SMTP at localhost:1025localhost:1025, see every sent email in a web UI. Never accidentally email real users from a dev script.
10. Sign and encrypt
S/MIME or PGP/MIME for confidential payloads. Heavy but standardized.
Anti-Spam / Anti-Abuse Notes
- Gmail caps your sending to ~500 messages/day for personal accounts.
- Sending more than a handful per minute will get you throttled.
- Marketing emails must include an unsubscribe link by law (CAN-SPAM, GDPR).
- Always include a real
FromFromname and reply-to that goes somewhere real.
Real-World Applications
- Account flows — signup confirmations, password resets, 2FA codes.
- System alerts — Cron job failed, disk filling up.
- Marketing — newsletters, drip campaigns (use a provider for these).
- Reports — daily summaries from a script.
- Internal tooling — meeting reminders, expense approvals.
Educational Value
- SMTP protocol — the wire format underlying every “send email” library.
- MIME — multipart messages, content types, transfer encodings.
- Secrets management — environment variables, keyring, dotenv.
- Validation — regex for email syntax (note: regex cannot fully validate; SMTP verification is the only certain way).
- Provider quirks — every SMTP server has its own gotchas; abstract the differences.
Next Steps
- Move credentials into environment variables or
keyringkeyring. - Add attachments and HTML body.
- Wire up a CSV-driven mail merge.
- Replace
smtplibsmtplibwithyagmailyagmailfor a cleaner Gmail-specific API. - Move to SendGrid or Mailgun for transactional use.
Conclusion
You built a real email-sending tool from scratch, learned why Gmail demands App Passwords, and saw how MIME multiparts model attachments and HTML bodies. The same patterns scale all the way up to corporate email pipelines — smtplibsmtplib is the standard tool from beginner scripts to systems sending millions of messages a day. Full source on GitHub. Find more automation projects on Python Central Hub.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
