Yagmail
Send email in Python conveniently for gmail using yagmail
Install / Use
/learn @kootenpv/YagmailREADME
yagmail -- Yet Another GMAIL/SMTP client
For the asynchronous asyncio version, look here: https://github.com/kootenpv/aioyagmail
The goal here is to make it as simple and painless as possible to send emails.
In the end, your code will look something like this:
import yagmail
yag = yagmail.SMTP('mygmailusername', 'mygmailpassword')
contents = ['This is the body, and here is just text http://somedomain/image.png',
'You can find an audio file attached.', '/local/path/song.mp3']
yag.send('to@someone.com', 'subject', contents)
In 2020, I personally prefer: using an Application-Specific Password
Table of Contents
|Section|Explanation| |---------------------------------------------------------------|---------------------------------------------------------------------| |Install | Find the instructions on how to install yagmail here | |Start a connection | Get started | |Usability | Shows some usage patterns for sending | |Recipients | How to send to multiple people, give an alias or send to self | |Magical contents | Really easy to send text, html, images and attachments | |Attaching files | How attach files to the email | |DKIM Support | Add DKIM signature to your emails with your private key | |Feedback | How to send me feedback | |Roadmap (and priorities) | Yup | |Errors | List of common errors for people dealing with sending emails |
Install
For Python 2.x and Python 3.x respectively:
pip install yagmail[all]
pip3 install yagmail[all]
As a side note, yagmail can now also be used to send emails from the command line.
Start a connection
yag = yagmail.SMTP('mygmailusername', 'mygmailpassword')
Note that this connection is reusable, closable and when it leaves scope it will clean up after itself in CPython.
As tilgovi points out in #39, SMTP does not automatically close in PyPy. The context manager with should be used in that case.
Usability
Defining some variables:
to = 'santa@someone.com'
to2 = 'easterbunny@someone.com'
to3 = 'sky@pip-package.com'
subject = 'This is obviously the subject'
body = 'This is obviously the body'
html = '<a href="https://pypi.python.org/pypi/sky/">Click me!</a>'
img = '/local/file/bunny.png'
All variables are optional, and know that not even to is required (you'll send an email to yourself):
yag.send(to = to, subject = subject, contents = body)
yag.send(to = to, subject = subject, contents = [body, html, img])
yag.send(contents = [body, img])
Furthermore, if you do not want to be explicit, you can do the following:
yag.send(to, subject, [body, img])
Recipients
It is also possible to send to a group of people by providing a list of email strings rather than a single string:
yag.send(to = to)
yag.send(to = [to, to2]) # List or tuples for emailadresses *without* aliases
yag.send(to = {to : 'Alias1'}) # Dictionary for emailaddress *with* aliases
yag.send(to = {to : 'Alias1', to2 : 'Alias2'}
Giving no to argument will send an email to yourself. In that sense, yagmail.SMTP().send() can already send an email.
Be aware that if no explicit to = ... is used, the first argument will be used to send to. Can be avoided like:
yag.send(subject = 'to self', contents = 'hi!')
Note that by default all email addresses are conservatively validated using soft_email_validation==True (default).
Oauth2
It is even safer to use Oauth2 for authentication, as you can revoke the rights of tokens.
This is one of the best sources, upon which the oauth2 code is heavily based.
The code:
yag = yagmail.SMTP("user@gmail.com", oauth2_file="~/oauth2_creds.json")
yag.send(subject="Great!")
It will prompt for a google_client_id and a google_client_secret, when the file cannot be found. These variables can be obtained following the previous link.
After you provide them, a link will be shown in the terminal that you should followed to obtain a google_refresh_token. Paste this again, and you're set up!
Note that people who obtain the file can send emails, but nothing else. As soon as you notice, you can simply disable the token.
Preventing OAuth authorization from expiring after 7 days
Your Google Cloud Platform project's OAuth consent screen must be in "In production" publishing status before authorizing to not have the authorization expire after 7 days. See status at https://console.cloud.google.com/apis/credentials/consent
Your OAuth client ID must be of type "Desktop". Check at https://console.cloud.google.com/apis/credentials
Magical contents
The contents argument will be smartly guessed. It can be passed a string (which will be turned into a list); or a list. For each object in the list:
- If it is a dictionary it will assume the key is the content, and the value is an alias (only for images currently!) e.g. {'/path/to/image.png' : 'MyPicture'}
- It will try to see if the content (string) can be read as a file locally, e.g. '/path/to/image.png'
- if impossible, it will check if the string is valid html
e.g.
<h1>This is a big title</h1> - if not, it must be text. e.g. 'Hi Dorika!'
Note that local files can be html (inline); everything else will be attached.
Local files require to have an extension for their content type to be inferred.
As of version 0.4.94, raw and inline have been added.
rawensures a string will not receive any "magic" (inlining, html, attaching)inlinewill make an image appear in the text.
Attaching Files
There are multiple ways to attach files in the attachments parameter (in addition to magical contents parameter).
- One can pass a list of paths i.e.
yag.send(to=recipients,
subject=email_subject,
contents=contents,
attachments=['path/to/attachment1.png', 'path/to/attachment2.pdf', 'path/to/attachment3.zip']
)
- One can pass an instance of
io.IOBase.
with open('path/to/attachment', 'rb') as f:
yag.send(to=recipients,
subject=email_subject,
contents=contents,
attachments=f
)
In this example f is an instance of _io.BufferedReader a subclass of the abstract class io.IOBase.
f has in this example the attribute .name, which is used by yagmail as filename as well as to detect the correct MIME-type.
Not all io.IOBase instances have the .name attribute in which case yagmail names the attachments attachment1, attachment2, ... without a file extension!
Therefore, it is highly recommended setting the filename with extension manually e.g. f.name = 'my_document.pdf'
A real-world example would be if the attachment is retrieved from a different source than the disk (e.g. downloaded from the internet or uploaded by a user in a web-application)
DKIM Support
To send emails with dkim signature, you need to install the package with all related packages.
pip install yagmail[all]
# or
pip install yagmail[dkim]
Usage:
from yagmail import SMTP
from yagmail.dkim import DKIM
from pathlib import Path
# load private key from file/secrets manager
private_key = Path("privkey.pem").read_bytes()
dkim_obj = DKIM(
domain=b"a.com",
selector=b"selector",
private_key=private_key,
include_headers=[b"To", b"From", b"Subject"],
# To include all default headers just pass None instead
# include_headers=None,
)
yag = SMTP(dkim=dkim_obj)
# all the rest is the same
Feedback
I'll try to respond to issues within 24 hours at Github.....
And please send me a line of feedback with SMTP().feedback('Great job!') :-)
Roadmap (and priorities)
- ~~Added possibility of Image~~
- ~~Optional SMTP arguments should go with **kwargs to my SMTP~~
- ~~CC/BCC (high)~~
- ~~Custom names (high)~~
- ~~Allow send to return a preview rather than to actually send~~
- ~~Just use attachments in "contents", being smart guessed (high, complex)~~
- ~~Attachments (contents) in a list so they actually define the order (medium)~~
- ~~Use lxml to see if it can parse the html (low)~~
- ~~Added tests (high)~~
- ~~Allow caching of content (low)~~
- ~~Extra other types (low)~~ (f
Related Skills
node-connect
344.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
claude-opus-4-5-migration
99.2kMigrate prompts and code from Claude Sonnet 4.0, Sonnet 4.5, or Opus 4.1 to Opus 4.5
frontend-design
99.2kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
model-usage
344.4kUse CodexBar CLI local cost usage to summarize per-model usage for Codex or Claude, including the current (most recent) model or a full model breakdown. Trigger when asked for model-level usage/cost data from codexbar, or when you need a scriptable per-model summary from codexbar cost JSON.
