-
Notifications
You must be signed in to change notification settings - Fork 70
/
Copy pathpymailer.py
executable file
·289 lines (235 loc) · 9.87 KB
/
pymailer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
#!/usr/bin/env python
import csv
from datetime import datetime
from email import message
import logging
import os
import re
import smtplib
import sys
from time import sleep
import config
# Setup logging to specified log file
logging.basicConfig(filename=config.LOG_FILENAME, level=logging.DEBUG)
class PyMailer():
"""
A python bulk mailer commandline utility. Takes five arguments: the path to the html file to be parsed; the
database of recipients (.csv); the subject of the email; email adsress the mail comes from; and the name the email
is from.
"""
def __init__(self, html_path, csv_path, subject, *args, **kwargs):
self.html_path = html_path
self.csv_path = csv_path
self.subject = subject
self.from_name = kwargs.get('from_name', config.FROM_NAME)
self.from_email = kwargs.get('to_name', config.FROM_EMAIL)
def _stats(self, message):
"""
Update stats log with: last recipient (incase the server crashes); datetime started; datetime ended; total
number of recipients attempted; number of failed recipients; and database used.
"""
try:
stats_file = open(config.STATS_FILE, 'r')
except IOError:
raise IOError("Invalid or missing stats file path.")
stats_entries = stats_file.read().split('\n')
# Check if the stats entry exists if it does overwrite it with the new message
is_existing_entry = False
if stats_entries:
for i, entry in enumerate(stats_entries):
if entry:
if message[:5] == entry[:5]:
stats_entries[i] = message
is_existing_entry = True
# If the entry does not exist append it to the file
if not is_existing_entry:
stats_entries.append(message)
stats_file = open(config.STATS_FILE, 'w')
for entry in stats_entries:
if entry:
stats_file.write("%s\n" % entry)
stats_file.close()
def _validate_email(self, email_address):
"""
Validate the supplied email address.
"""
if not email_address or len(email_address) < 5:
return None
if not re.match(r'^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$', email_address):
return None
return email_address
def _retry_handler(self, recipient_data):
"""
Write failed recipient_data to csv file to be retried again later.
"""
try:
csv_file = open(config.CSV_RETRY_FILENAME, 'wb+')
except IOError:
raise IOError("Invalid or missing csv file path.")
csv_writer = csv.writer(csv_file)
csv_writer.writerow([
recipient_data.get('name'),
recipient_data.get('email')
])
csv_file.close()
def _html_parser(self, recipient_data):
"""
Open, parse and substitute placeholders with recipient data.
"""
try:
html_file = open(self.html_path, 'rb')
except IOError:
raise IOError("Invalid or missing html file path.")
html_content = html_file.read()
if not html_content:
raise Exception("The html file is empty.")
# Replace all placeolders associated to recipient_data keys
if recipient_data:
for key, value in recipient_data.items():
placeholder = "<!--%s-->" % key
html_content = html_content.replace(placeholder, value)
return html_content
def _form_email(self, recipient_data):
"""
Form the html email, including mimetype and headers.
"""
# Form the recipient and sender headers
recipient = "%s <%s>" % (recipient_data.get('name'), recipient_data.get('email'))
sender = "%s <%s>" % (self.from_name, self.from_email)
# Get the html content
html_content = self._html_parser(recipient_data)
# Instatiate the email object and assign headers
email_message = message.Message()
email_message.add_header('From', sender)
email_message.add_header('To', recipient)
email_message.add_header('Subject', self.subject)
email_message.add_header('MIME-Version', '1.0')
email_message.add_header('Content-Type', 'text/html')
email_message.set_payload(html_content)
return email_message.as_string()
def _parse_csv(self, csv_path=None):
"""
Parse the entire csv file and return a list of dicts.
"""
is_resend = csv_path is not None
if not csv_path:
csv_path = self.csv_path
try:
csv_file = open(csv_path, 'rwb')
except IOError:
raise IOError("Invalid or missing csv file path.")
csv_reader = csv.reader(csv_file)
recipient_data_list = []
for i, row in enumerate(csv_reader):
# Test indexes exist and validate email address
try:
recipient_name = row[0]
recipient_email = self._validate_email(row[1])
except IndexError:
recipient_name = ''
recipient_email = None
print(recipient_name, recipient_email)
# Log missing email addresses and line number
if not recipient_email:
logging.error("Recipient email missing in line %s" % i)
else:
recipient_data_list.append({
'name': recipient_name,
'email': recipient_email,
})
# Clear the contents of the resend csv file
if is_resend:
csv_file.write('')
csv_file.close()
return recipient_data_list
def send(self, retry_count=0, recipient_list=None):
"""
Iterate over the recipient list and send the specified email.
"""
if not recipient_list:
recipient_list = self._parse_csv()
if retry_count:
recipient_list = self._parse_csv(config.CSV_RETRY_FILENAME)
# Save the number of recipient and time started to the stats file
if not retry_count:
self._stats("TOTAL RECIPIENTS: %s" % len(recipient_list))
self._stats("START TIME: %s" % datetime.now())
# Instantiate the number of falied recipients
failed_recipients = 0
for recipient_data in recipient_list:
# Instantiate the required vars to send email
message = self._form_email(recipient_data)
if recipient_data.get('name'):
recipient = "%s <%s>" % (recipient_data.get('name'), recipient_data.get('email'))
else:
recipient = recipient_data.get('email')
sender = "%s <%s>" % (self.from_name, self.from_email)
# Send the actual email
smtp_server = smtplib.SMTP(host=config.SMTP_HOST, port=config.SMTP_PORT)
try:
smtp_server.sendmail(sender, recipient, message)
# Save the last recipient to the stats file incase the process fails
self._stats("LAST RECIPIENT: %s" % recipient)
# Allow the system to sleep for .25 secs to take load off the SMTP server
sleep(0.25)
except:
logging.error("Recipient email address failed: %s" % recipient)
self._retry_handler(recipient_data)
# Save the number of failed recipients to the stats file
failed_recipients = failed_recipients + 1
self._stats("FAILED RECIPIENTS: %s" % failed_recipients)
def send_test(self):
self.send(recipient_list=config.TEST_RECIPIENTS)
def resend_failed(self):
"""
Try and resend to failed recipients two more times.
"""
for i in range(1, 3):
self.send(retry_count=i)
def count_recipients(self, csv_path=None):
return len(self._parse_csv(csv_path))
def main(sys_args):
if not os.path.exists(config.CSV_RETRY_FILENAME):
open(config.CSV_RETRY_FILENAME, 'wb').close()
if not os.path.exists(config.STATS_FILE):
open(config.STATS_FILE, 'wb').close()
try:
action, html_path, csv_path, subject = sys_args
except ValueError:
print("Not enough argumants supplied. PyMailer requests 1 option and 3 arguments: ./pymailer -s html_path csv_path subject")
sys.exit()
if os.path.splitext(html_path)[1] != '.html':
print("The html_path argument doesn't seem to contain a valid html file.")
sys.exit()
if os.path.splitext(csv_path)[1] != '.csv':
print("The csv_path argument doesn't seem to contain a valid csv file.")
sys.exit()
pymailer = PyMailer(html_path, csv_path, subject)
if action == '-s':
confirmation = raw_input(
"You are about to send to {} recipients. Do you want to continue (yes/no)? ".format(pymailer.count_recipients())
)
if confirmation in ['yes', 'y']:
# Save the csv file used to the stats file
pymailer._stats("CSV USED: %s" % csv_path)
# Send the email and try resend to failed recipients
pymailer.send()
pymailer.resend_failed()
else:
print("Aborted.")
sys.exit()
elif action == '-t':
confirmation = raw_input(
"You are about to send a test mail to all recipients as specified in config.py. Do you want to continue (yes/no)? "
)
if confirmation in ['yes', 'y']:
pymailer.send_test()
else:
print("Aborted.")
sys.exit()
else:
print("{} option is not support. Use either [-s] to send to all recipients or [-t] to send to test recipients".format(action))
# Save the end time to the stats file
pymailer._stats("END TIME: %s" % datetime.now())
if __name__ == '__main__':
main(sys.argv[1:])