Skip to content

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

Set Up Gmail App Password

  1. Visit myaccount.google.com/security.
  2. Enable 2-Step Verification (required to access App Passwords).
  3. Go to App passwords → generate a new one named “Python Mailer”.
  4. Copy the 16-character code. This replaces your normal password in scripts.
  5. Treat it like a password. Anyone with this string can send mail as you.

Getting Started

Create the project

  1. Create folder email-senderemail-sender.
  2. Inside, create emailsender.pyemailsender.py.

Write the code

Email SenderSource
Email Sender
# 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()
 
Email Sender
# 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

command
C:\Users\Your Name\email-sender> python emailsender.py
# GUI opens with sender / receiver / password / message fields and Send button
command
C:\Users\Your Name\email-sender> python emailsender.py
# GUI opens with sender / receiver / password / message fields and Send button

SMTP in Seven Lines

The core is shorter than the GUI around it:

core.py
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.")
core.py
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:

  1. Connect to Gmail’s SMTP on port 587.
  2. STARTTLSSTARTTLS upgrades the connection to encrypted.
  3. Login with your address + App Password.
  4. sendmailsendmail delivers the message.
  5. The withwith block automatically issues QUITQUIT on 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

emailsender.py
import tkinter as tk
from tkinter import messagebox
import smtplib, re
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
emailsender.py
import tkinter as tk
from tkinter import messagebox
import smtplib, re
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

2. Build the form

emailsender.py
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)
emailsender.py
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

emailsender.py
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.")
emailsender.py
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.py
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.py
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

attach.py
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")
attach.py
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:

env.py
import os
sender = os.environ["EMAIL_USER"]
password = os.environ["EMAIL_PASSWORD"]
env.py
import os
sender = os.environ["EMAIL_USER"]
password = os.environ["EMAIL_PASSWORD"]

Set them once:

bash
export EMAIL_USER=you@gmail.com
export EMAIL_PASSWORD=xxxx-xxxx-xxxx-xxxx
bash
export EMAIL_USER=you@gmail.com
export EMAIL_PASSWORD=xxxx-xxxx-xxxx-xxxx

Or 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:

keychain.py
import keyring
keyring.set_password("PCH-mailer", sender, app_password)     # one-time setup
pwd = keyring.get_password("PCH-mailer", sender)             # later
keychain.py
import keyring
keyring.set_password("PCH-mailer", sender, app_password)     # one-time setup
pwd = keyring.get_password("PCH-mailer", sender)             # later

Common Mistakes

ProblemCauseFix
“Username and Password not accepted”Used real Gmail passwordGenerate an App Password
Mail goes to SpamNo proper FromFrom, no DKIM, plain-text bodyUse SendGrid/Mailgun for production
ConnectionRefusedErrorConnectionRefusedErrorWrong port587 for STARTTLS, 465 for smtplib.SMTP_SSLsmtplib.SMTP_SSL
Subject missingDid not add the headerSet msg["Subject"]msg["Subject"]
Emoji breaks subjectDefault encoding mismatchUse MIMEText(body, _charset="utf-8")MIMEText(body, _charset="utf-8")
Multiple recipients only first receivesPassed string, not listSplit on commas and pass a list

Variations to Try

1. CC and BCC

cc_bcc.py
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)
cc_bcc.py
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

merge.py
for row in csv.DictReader(open("contacts.csv")):
    body = template.format(**row)
    send(row["email"], body)
merge.py
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

cli
mailer.py --to friend@example.com --subject Hi --body "Hello!" --attach report.pdf
cli
mailer.py --to friend@example.com --subject Hi --body "Hello!" --attach report.pdf

9. 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 FromFrom name 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 smtplibsmtplib with yagmailyagmail for 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 coffee

Was this page helpful?

Let us know how we did