Skip to content

Instantly share code, notes, and snippets.

@JavaScriptDude
Last active October 25, 2023 19:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JavaScriptDude/d540947ac0cca1d7e13e42df9eb6c269 to your computer and use it in GitHub Desktop.
Save JavaScriptDude/d540947ac0cca1d7e13e42df9eb6c269 to your computer and use it in GitHub Desktop.
SMTP Email in Python 3.7+
import smtplib
import os
import json
from email.message import EmailMessage
from validate_email import validate_email
class Mailer():
SMTP_USER: str
SMTP_PASS: str
LOGGER: callable
def __init__(self, smtp_user: str, smtp_pass: str, smtp_server:str='smtp.gmail.com'):
assert isinstance(smtp_user, str) and len(smtp_user.strip()) > 0, "smtp_user must be a non-empty string"
assert isinstance(smtp_pass, str) and len(smtp_pass.strip()) > 0, "smtp_user must be a non-empty string"
assert isinstance(smtp_server, str) and len(smtp_server.strip()) > 0, "smtp_user must be a non-empty string"
self.SMTP_USER = smtp_user
self.SMTP_PASS = smtp_pass
self.SMTP_SERVER = smtp_server
def send_mail(self , subject: str
, body: str
, to_list: list
, cc_list: list=None
, bcc_list: list=None
, from_addr: str = None
, attachments:list = None
, as_html:bool=False
, dry_run: bool=False
, do_print: bool=False):
pc: callable = self.LOGGER
charset = "utf-8"
if not from_addr is None and from_addr.lower() != self.SMTP_USER.lower():
assert self.SMTP_SERVER.lower() != 'smtp.gmail.com', f"Sorry, for gmail, you cannot specify a different from_addr"
else:
assert self.SMTP_SERVER.lower() != 'smtp.sendgrid.net', \
f"For sendgrid, you must specify a from_addr!"
from_addr = self.SMTP_USER
if to_list is None:
raise Exception("to_list must not be None")
for _grp, _list in [('to_list', to_list), ('cc_list', cc_list), ('bcc_list', bcc_list)]:
if _list and len(_list) > 0:
for _addr in _list:
assert validate_email(_addr), f"Invalid email address in {_grp}: `{_addr}`"
assert validate_email(from_addr), f"Invalid email address in from_addr: `{from_addr}`"
to_list_all = to_list
if cc_list is not None:
to_list_all = to_list_all + cc_list
if bcc_list is not None:
to_list_all = to_list_all + bcc_list
if do_print:
print(f"""\n
| Mail to be sent:
| from: {from_addr}
| to: {to_list}
| subject: {subject}
| message:\n
| {body}
~
""".replace("&nbsp;",' ').replace("<br>",'\n'))
_mail = EmailMessage()
_mail['Subject'] = subject
# Set body
if as_html:
_mail.set_content(body, subtype='html')
else:
_mail.set_content(body)
_mail['From'] = from_addr
_mail['To'] = ', '.join(to_list)
if cc_list and len(cc_list) > 0:
_mail['Cc'] = ', '.join(cc_list)
if bcc_list and len(bcc_list) > 0:
_mail['Bcc'] = ', '.join(bcc_list)
if attachments:
assert isinstance(attachments, list), "attachments must be a list"
attachment:Attach=None
for attachment in attachments:
attachment.add_to_email(_mail)
# print('''Mail to be sent:
# | from: {}
# | to: {}
# | message: {}
# | raw:\n{}
# | ~
# '''.format(from_addr, to_list, body, quopri.decodestring(message.as_string()))
# )
try:
# Connect to smtp server
smtp = smtplib.SMTP(self.SMTP_SERVER, 587)
# Some servers insist on this
smtp.ehlo()
# Upgrade TLS
smtp.starttls()
# Some servers insist on this
smtp.ehlo()
# Log in
smtp.login(self.SMTP_USER, self.SMTP_PASS)
except:
raise self.AuthException("Failed while logging into smtp relay")
if dry_run:
print("IN DRYRUN - Not sending out email")
else:
smtp.send_message(_mail)
smtp.quit()
print(f'smtp.send_message() called with no errors - (subj: {subject})')
def __bool__(self): return True
def assert_is_not_blank(s, where) -> str:
assert isinstance(s, str), "String not passed in {where}. Got: {}".format(type(s))
assert not s.strip() == '', "Empty string passed in {where}"
return s
# usage: file,path = Q_.splitPath(s)
def splitPath(s):
assert isinstance(s, str), "String not passed. Got: {}".format(type(s))
s = s.strip()
assert not s == '', "Empty string passed"
f = os.path.basename(s)
if len(f) == 0: return ('', s[:-1] if s[-1:] == '/' else s)
p = s[:-(len(f))-1]
return f, p
""" Attach For others see:
. https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
. https://learn.microsoft.com/en-us/archive/blogs/vsofficedeveloper/office-2007-file-format-mime-types-for-http-content-streaming-2
att = Q_.Attach("/foo/bar/baz.xls", 'application/vnd.ms-excel'))
"""
class Attach():
Path:str = None
Name:str = None
MIME:str = None
def __init__(self, Path:str, MIME:str, Name:str=None):
self.Path = Path
assert os.path.isfile(Path), f"Attachment path not found: '{Path}'"
self.MIME = MIME
if Name is None:
self.Name, _ = splitPath(Path)
else:
self.Name = Name
def add_to_email(self, msg:EmailMessage):
assert isinstance(msg, EmailMessage)
(_maintype, _subtype) = self.MIME.split('/')
with open(self.Path, "rb") as f:
msg.add_attachment(f.read(), maintype=_maintype, subtype=_subtype, filename=self.Name)
def serialize(self) -> str:
return json.dumps(dict(Path = self.Path
, MIME = self.MIME
, Name = self.Name))
@classmethod
def deserialize(cls, att_data:dict) -> 'Attach':
assert isinstance(att_data, dict)
_mtype = assert_is_not_blank(att_data['MIME'], "att_data['MIME']")
if _mtype == '':
_mtype = 'text/plain'
else:
if not len(_mtype.split('/')) == 2:
print(f"Invalid mime: '{_mtype}'. Defaulting to text/plain")
_mtype = 'text/plain'
return Attach(Path = assert_is_not_blank(att_data['Path'], "att_data['Path']")
, MimeType = _mtype
, Name = assert_is_not_blank(att_data['Name'], "att_data['Name']"))
@JavaScriptDude
Copy link
Author

This is a example of how to do SMTP Emails in modern Python. The Attach class helps the management of attachments be much smoother than OOTB Python.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment