Skip to content

Commit 51fee3c

Browse files
authored
Merge pull request #9 from ramanaditya/aws-ses
Aws ses
2 parents 5ce9037 + 49aad83 commit 51fee3c

File tree

7 files changed

+256
-6
lines changed

7 files changed

+256
-6
lines changed

email_service/email_handler.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from email_service.sendgrid.sendgrid import SendgridMail
21
from email_service.settings import Settings
32
from email_service.utils import constants
43

@@ -10,9 +9,27 @@ def __init__(self, email_type="INDIVIDUAL") -> None:
109
raise ValueError("Invalid Email type is passed")
1110

1211
def sendgrid(self, data: dict, api_key=None) -> dict:
12+
from email_service.sendgrid.sendgrid import SendgridMail
13+
1314
API_KEY = Settings().SENDGRID_API_KEY or api_key
1415
if not API_KEY:
1516
raise KeyError("SENDGRID_API_KEY not found in environment")
1617
sendgrid_mail = SendgridMail(API_KEY)
1718
response = sendgrid_mail.sendgrid_handler(data, email_type=self.EMAIL_TYPE)
1819
return response
20+
21+
def aws_ses(self, data: dict, aws_access_key_id: str = None, aws_secret_access_key: str = None,
22+
aws_region: str = None, smtp_host: str = None, port: str = None, charset: str = None) -> dict:
23+
from email_service.ses.aws_ses import SESMail
24+
25+
aws_access_key_id = Settings().AWS_ACCESS_KEY_ID or aws_access_key_id
26+
aws_secret_access_key = Settings().AWS_SECRET_ACCESS_KEY or aws_secret_access_key
27+
smtp_host = Settings().SMTP_HOST or smtp_host
28+
port = Settings().PORT or port
29+
aws_region = Settings().AWS_REGION or aws_region
30+
charset = charset or Settings().CHARSET
31+
if not aws_access_key_id or not aws_secret_access_key or not aws_region:
32+
raise KeyError("AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION not found")
33+
ses_mail = SESMail(aws_access_key_id, aws_secret_access_key, aws_region, charset, smtp_host, port)
34+
response = ses_mail.ses_handler(data, email_type=self.EMAIL_TYPE)
35+
return response

email_service/ses/__init__.py

Whitespace-only changes.

email_service/ses/aws_ses.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import smtplib
2+
from email.mime.application import MIMEApplication
3+
from email.mime.multipart import MIMEMultipart
4+
from email.mime.text import MIMEText
5+
6+
import boto3
7+
from botocore.exceptions import ClientError
8+
9+
from email_service.utils.clean_data import CleanMailingList
10+
from email_service.utils.functions import read_file_in_binary
11+
from email_service.utils.validators import Validation
12+
13+
14+
class SESMail:
15+
"""To send the email using AWS SES"""
16+
17+
def __init__(self, aws_access_key_id: str, aws_secret_access_key: str, aws_region: str, charset: str,
18+
smtp_host, port, smtp: bool = True) -> None:
19+
self.AWS_ACCESS_KEY_ID = aws_access_key_id
20+
self.AWS_SECRET_ACCESS_KEY = aws_secret_access_key
21+
self.AWS_REGION = aws_region
22+
self.CHARSET = charset
23+
self.SMTP = smtp
24+
self.SMTP_HOST = smtp_host
25+
self.PORT = port
26+
27+
@staticmethod
28+
def add_attachment(data: list) -> list:
29+
attached_files = []
30+
for single_file in data:
31+
disposition = "attachment"
32+
single_file = single_file
33+
34+
file_name, extension = single_file.split(".")
35+
36+
binary_content = read_file_in_binary(single_file)
37+
38+
attachment = MIMEApplication(binary_content)
39+
attachment.add_header('Content-Disposition', disposition, filename=f"{file_name}.{extension}")
40+
41+
attached_files.append(attachment)
42+
return attached_files
43+
44+
def form_message(
45+
self,
46+
sub: str = "Default Subject",
47+
reply_to_addresses=None,
48+
html_body: str = None,
49+
text_body=None,
50+
to=None,
51+
from_email=None,
52+
cc=None,
53+
bcc=None,
54+
attachments=None,
55+
return_path_address: str = None
56+
):
57+
"""Forming the mail to be send"""
58+
# Handle the default values of params
59+
if attachments is None:
60+
attachments = []
61+
if bcc is None:
62+
bcc = []
63+
if cc is None:
64+
cc = []
65+
if to is None:
66+
to = []
67+
if reply_to_addresses is None:
68+
reply_to_addresses = []
69+
70+
message = MIMEMultipart("mixed")
71+
message["To"] = ", ".join([f"{emails.get('name')} <{emails.get('email')}>" for emails in to])
72+
message["Cc"] = ", ".join([f"{emails.get('name')} <{emails.get('email')}>" for emails in cc])
73+
message["Bcc"] = ", ".join([f"{emails.get('name')} <{emails.get('email')}>" for emails in bcc])
74+
message["Subject"] = sub
75+
message["From"] = from_email
76+
if reply_to_addresses:
77+
message['Reply-To'] = reply_to_addresses[0]
78+
if return_path_address:
79+
message["Return-Path"] = return_path_address
80+
81+
message_subtype = "alternative" if text_body and html_body else "mixed"
82+
message_body = MIMEMultipart(message_subtype)
83+
84+
if text_body:
85+
text_body_content = MIMEText(text_body.encode(self.CHARSET), "plain", self.CHARSET)
86+
message_body.attach(text_body_content)
87+
if html_body:
88+
html_body_content = MIMEText(html_body.encode(self.CHARSET), "html", self.CHARSET)
89+
message_body.attach(html_body_content)
90+
message.attach(message_body)
91+
92+
if attachments:
93+
attachments_list = self.add_attachment(attachments)
94+
for attachment in attachments_list:
95+
message.attach(attachment)
96+
97+
return message
98+
99+
def send_email(self, message, sender: str, recipients: list) -> dict:
100+
try:
101+
if self.SMTP:
102+
server = smtplib.SMTP(host=self.SMTP_HOST, port=self.PORT)
103+
server.ehlo()
104+
server.starttls()
105+
# stmplib docs recommend calling ehlo() before & after starttls()
106+
server.ehlo()
107+
108+
server.login(self.AWS_ACCESS_KEY_ID, self.AWS_SECRET_ACCESS_KEY)
109+
110+
response = server.sendmail(
111+
sender,
112+
", ".join([f"{email_dict.get('name')} <{email_dict.get('email')}>" for email_dict in recipients]),
113+
message.as_string())
114+
server.close()
115+
116+
return {
117+
"status_code": 202,
118+
"message": f"{response}",
119+
}
120+
121+
else:
122+
client = boto3.client('ses',
123+
aws_access_key_id=self.AWS_ACCESS_KEY_ID,
124+
aws_secret_access_key=self.AWS_SECRET_ACCESS_KEY,
125+
region_name=self.AWS_REGION)
126+
response = client.send_raw_email(
127+
Source=sender,
128+
Destinations=[email_dict.get("email") for email_dict in recipients],
129+
RawMessage={
130+
'Data': message.as_string(),
131+
}
132+
)
133+
134+
return {
135+
"status_code": 202,
136+
"message": f"{response['MessageId']}",
137+
}
138+
except ClientError as e:
139+
return {
140+
"status_code": 400,
141+
"message": f"{e.response['Error']['Message']}",
142+
}
143+
except Exception as e:
144+
status = {
145+
"status_code": 400,
146+
"message": f"Something Went Wrong: {e}",
147+
}
148+
return status
149+
150+
def bulk(self, data: dict) -> dict:
151+
"""
152+
Bulk Email : Email will be sent in a bulk of (500 for sendgrid) including all the to, cc, bcc
153+
:param data:
154+
:return:
155+
"""
156+
157+
# Cleaning the data
158+
event = CleanMailingList().clean_mailing_list(data)
159+
160+
# For returning Status
161+
status = dict()
162+
163+
# For sending Bulk Mail
164+
event["recipients"]["bcc"].extend(event["recipients"]["to"])
165+
event["recipients"]["to"] = None
166+
cc_count = len(event["recipients"]["cc"])
167+
bcc_count = len(event["recipients"]["bcc"])
168+
batch = 500 - cc_count - 1 # -1 for 'to' email
169+
for emails in range(0, bcc_count, batch):
170+
message = self.form_message(
171+
sub=event["subject"],
172+
reply_to_addresses=event["reply_to_addresses"],
173+
html_body=event["html_body"],
174+
text_body=event["text_body"],
175+
from_email=event["from_email"],
176+
to=event["to_for_bulk"],
177+
cc=event["recipients"]["cc"],
178+
bcc=event["recipients"]["bcc"][emails: emails + batch],
179+
attachments=event["attachments"],
180+
)
181+
status = self.send_email(message)
182+
183+
return status
184+
185+
def individual(self, data) -> dict:
186+
"""
187+
Individual Email : All the emails will be sent separately, if mentioned in to and
188+
cc,bcc will be attached with all the emails
189+
:param data:
190+
:return status:
191+
"""
192+
# Cleaning the data
193+
event = CleanMailingList().clean_mailing_list(data)
194+
195+
# Storing Status
196+
status = dict()
197+
198+
# Sending Individual Mail
199+
for email_id in event["recipients"]["to"]:
200+
message = self.form_message(
201+
sub=event["subject"],
202+
reply_to_addresses=event["reply_to_addresses"],
203+
html_body=event["html_body"],
204+
text_body=event["text_body"],
205+
to=[email_id],
206+
from_email=event["from_email"],
207+
cc=event["recipients"]["cc"],
208+
bcc=event["recipients"]["bcc"],
209+
attachments=event["attachments"],
210+
return_path_address=event["return_path_address"]
211+
)
212+
status = self.send_email(message, event["from_email"], [email_id])
213+
return status
214+
215+
def ses_handler(self, data: dict, email_type="INDIVIDUAL") -> dict:
216+
validation = Validation().validation(data)
217+
218+
if "attachments" in data.keys() and len(data["attachments"]) > 0:
219+
Validation().file_existence(data["attachments"])
220+
221+
if validation["status_code"] == 200:
222+
status = dict()
223+
if email_type == "INDIVIDUAL":
224+
status = self.individual(data)
225+
else:
226+
status = self.bulk(data)
227+
return status
228+
else:
229+
return validation

email_service/settings.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33

44
class Settings:
5-
CHARSET = None
6-
SENDGRID_API_KEY = None
7-
85
def __init__(self):
96
self.CHARSET = os.environ.get("CHARSET", default="utf-8")
107
self.SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY")
8+
self.AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
9+
self.AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
10+
self.AWS_REGION = os.environ.get("AWS_REGION")
11+
self.SMTP_HOST = os.environ.get("SMTP_HOST")
12+
self.PORT = os.environ.get("PORT")

email_service/utils/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""This file contains the constants used in the module"""
22
AVAILABLE_EMAIL_TYPES = ["INDIVIDUAL", "BULK"]
33
ATTACHMENT_FILE_TYPES = {
4+
"txt": "text/plain",
45
"ics": "text/calendar",
56
"jpeg": "image/jpeg",
67
"jpg": "image/jpeg",

email_service/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytz
55

6-
__version__ = "1.1.0"
6+
__version__ = "1.2.0"
77

88
# Add datetime.now() for test PyPI to skip conflicts in file name
99
test_version = os.environ.get("TESTPYPI", default=False)

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Email Clients
22
# =======================================================================
3-
sendgrid==6.4.8 # https://github.com/sendgrid/sendgrid-python/
3+
sendgrid==6.4.8 # https://github.com/sendgrid/sendgrid-python/
4+
boto3==1.17.98 # https://boto3.amazonaws.com/v1/documentation/api/latest/index.html
45

56
# Others
67
# =======================================================================

0 commit comments

Comments
 (0)