Last active
October 25, 2023 19:47
-
-
Save JavaScriptDude/d540947ac0cca1d7e13e42df9eb6c269 to your computer and use it in GitHub Desktop.
SMTP Email in Python 3.7+
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(" ",' ').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']")) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.