etho2020_registration/email_client.py
2019-10-16 09:38:46 +02:00

360 lines
13 KiB
Python

#!/usr/bin/env python3
import sys
import io
import imaplib
import os
import email
import email.header
import smtplib
from email.message import EmailMessage
import re
import datetime as dt
import time
import glob
from credentials import *
PARTICIPANTS_FOLDER = 'participants'
reg_fees = {'early': {"proMember": 95, "proNonMember":115, "studentMember": 55, "studentNonMember": 65},
'late': {"proMember": 115, "proNonMember":135, "studentMember": 65, "studentNonMember": 75}}
farewell_fee = {"senior": 45, "student": 25}
early_bird = d = dt.datetime.strptime("2020-01-16T00:00:00", "%Y-%m-%dT%H:%M:%S")
class Participation(object):
field_mapping = [("First name", "_first_name"), ("Last name", "_last_name"),
("Institution", "_institution", True), ("Street", "_address_street"),
("City", "_address_city"), ("ZIP code", "_address_zip"),
("Phone", "_phone"), ("e-Mail", "_email"),
("Country", "_address_country"), ("Fee", "_role"),
("Messages", "_message"), ("Food preferences", "_food_preference"),
("Farewell event", "_farewell"), ("Icebreaker", "_icebreaker"), ("Gwinner", "_gwinner"),
("Lab Tour (Fri 21 Feb)", "_lab_tour", True)]
def __init__(self, email_message):
self._registration_date = ""
self._first_name = ""
self._last_name = ""
self._phone = ""
self._email = ""
self._farewell = ""
self._gwinner = ""
self._icebreaker = ""
self._lab_tour = ""
self._food_preference = ""
self._food_vegi = False
self._food_vegan = False
self._food_gluten = False
self._food_normal = True
self._address_street = ""
self._address_city = ""
self._address_zip = ""
self._address_country = ""
self._institution = ""
self._role = ""
self._message = ""
self._fee = 0.0
self._conflicts = []
self.__parse_message(email_message)
def __parse_message(self, message):
date_tuple = email.utils.parsedate_tz(message['Date'])
if date_tuple:
local_date = dt.datetime.fromtimestamp(
email.utils.mktime_tz(date_tuple))
self._registration_date = local_date
msg_body = message.get_payload()[0].as_string()
msg_body = re.sub(r'=20', ' ', msg_body)
msg_body = re.sub(r'=\n', '', msg_body)
msg_body = re.sub(r'=E2=82=AC', 'EUR', msg_body)
msg_body = re.sub(r'=C3=BC', 'ü', msg_body)
msg_body = re.sub(r'=C3=9C', 'Ü', msg_body)
msg_body = re.sub(r'=C3=A4', 'ä', msg_body)
msg_body = re.sub(r'=C3=84', 'Ä', msg_body)
msg_body = re.sub(r'=C3=B6', 'ö', msg_body)
msg_body = re.sub(r'=C3=96', 'Ö', msg_body)
msg_body = re.sub(r'=C3=9F', 'ss', msg_body)
msg_body = re.sub(r'&', 'and', msg_body)
lines = msg_body.split('\n')
for fm in Participation.field_mapping:
self.__parse_lines(lines, fm)
self.__process_food(lines)
self.__check()
def __check(self):
if "student" not in self._role and self.gwinner_award:
self._conflicts.append("Gwinner award selected but participant not student!")
if (self._food_vegan or self._food_gluten or self._food_vegi) and self._food_normal:
self._conflicts.append("Possible conflict in food preferences!")
def __conf_fee(self):
fee = .0
early_or_late = "early" if self.registration_date < early_bird else "late"
pro_or_stud = "senior" if "pro" in self._role else "student"
fee += reg_fees[early_or_late][self._role]
return fee
def __farewell_event_fee(self):
fee = .0
pro_or_stud = "senior" if "pro" in self._role else "student"
fee += farewell_fee[pro_or_stud] if self.farewell_event else 0.0
return fee
def __parse_lines(self, lines, field_map):
assert(len(field_map) >= 2)
ls = [l for l in lines if l.lower().startswith(field_map[0].lower())]
if len(ls) < 1:
return
if len(field_map) == 2:
setattr(self, field_map[1], ls[0].split(field_map[0])[-1].strip())
else:
setattr(self, field_map[1], ', '.join(map(lambda x:x.split(field_map[0])[-1].strip(), ls)))
def __process_food(self, lines):
if len(self._food_preference) > 0:
prefs = self._food_preference.lower()
self._food_normal = "no special" in prefs
self._food_vegan = "vegan" in prefs
self._food_vegi = "vegetarian" in prefs
self._food_gluten = "gluten" in prefs
@property
def registration_date(self):
return self._registration_date
@property
def conference_fee(self):
return self.__conf_fee()
@property
def farewell_event_fee(self):
return self.__farewell_event_fee()
@property
def amount_due(self):
return self.conference_fee + self.farewell_event_fee
@property
def farewell_event(self):
return len(self._farewell) > 0
@property
def gwinner_award(self):
return len(self._gwinner) > 0
@property
def labtour(self):
return [] if len(self._lab_tour.strip()) == 0 else self._lab_tour.split(",")
@property
def valid(self):
return len(self._conflicts) == 0
@property
def icebreaker(self):
return len(self._icebreaker) > 0
@property
def name(self):
return "%s %s" % (self._first_name, self._last_name)
@property
def inconsistencies(self):
return ", ".join(self._conflicts)
@property
def message(self):
return self._message
def save_to_csv(self, filename=None):
if not os.path.exists(PARTICIPANTS_FOLDER):
os.mkdir(PARTICIPANTS_FOLDER)
if not filename:
stub = os.path.join(PARTICIPANTS_FOLDER, "".join(self.name.split())) + "*"
matches = glob.glob(stub)
if len(matches) > 0:
filename = os.path.join(PARTICIPANTS_FOLDER, "".join(self.name.split())) + "_%i" % len(matches) + ".csv"
else:
filename = os.path.join(PARTICIPANTS_FOLDER, "".join(self.name.split())) + ".csv"
header = ["LastName", "FirstName", "eMail", "Phone", "Institute", "Street", "City",
"RegDate", "CareerStage", "Fee", "FarewellEvent", "GwinnerAward", "LabTour", "Icebreaker",
"FoodVegetarian", "FoodVegan", "FoodGlutenFree", "FoodNoPref"]
values = [self._last_name, self._first_name, self._email, self._phone, self._institution,
self._address_street, "%s %s" % (self._address_zip, self._address_city),
self.registration_date.isoformat(), self._role, str(self.amount_due), str(self.farewell_event),
str(self.gwinner_award), str(self.labtour), str(self.icebreaker), str(self._food_vegi), str(self._food_vegan),
str(self._food_gluten), str(self._food_normal)]
with io.open(filename, mode="w", encoding="UTF8") as fd:
fd.write("; ".join(header))
fd.write("\n")
fd.write("; ".join(values))
return filename
def __str__(self):
str = ""
str += "Name: %s %s\n" % (self._first_name, self._last_name)
str += "Institution: %s\n" % self._institution
str += "Phone: %s\n" % self._phone
str += "e-Mail: %s\n" % self._email
str += "Career stage: %s %s\n" % ("senior, " if "pro" in self._role else "student, ",
"non-member" if "NonMember" in self._role else "member of Ethological Soc.")
str += "Icebreaker and Registration meeting: %s\n" % ("Yes" if self.icebreaker else "No")
str += "Farewell event: %s\n" % ("Yes" if self.farewell_event else "No")
str += "Conference Fee: %3.2f EUR\n" % self.conference_fee
str += "Farewell event: %3.2f EUR\n" % self.farewell_event_fee
str += "Amount due: %3.2f EUR\n" % self.amount_due
str += "Lab tour: %s\n" % ( "No" if len(self.labtour) == 0 else "Yes, " + ", ".join(self.labtour))
str += "Gwinner award: %s\n" % ("Yes" if self.gwinner_award else "No")
str += "Food preferences: %s, %s, %s, %s\n" % ("no special preferences" if self._food_normal else "",
"vegetarian" if self._food_vegi else "",
"vegan" if self._food_vegan else "",
"gluten free" if self._food_gluten else "")
str += "Registration date: %s\n" % (self.registration_date.isoformat().replace("T", " "))
str += "Message:%s\n" % self._message
if not self.valid:
str += "Inconsistencies: %s\n" % self.inconsistencies
return str
def process_mailbox(M):
"""
Do something with emails messages in the folder.
For the sake of this example, print some headers.
"""
rv, data = M.search(None, "UNSEEN")
if rv != 'OK':
print("No messages found!", file=log_file)
return
for num in data[0].split():
rv, data = M.fetch(num, '(RFC822)')
if rv != 'OK':
print("ERROR getting message", num, file=log_file)
return
msg = email.message_from_bytes(data[0][1])
hdr = email.header.make_header(email.header.decode_header(msg['Subject']))
subject = str(hdr)
print('Message %s: %s' % (num, subject))
print('Raw Date:', msg['Date'])
# Now convert to local date-time
date_tuple = email.utils.parsedate_tz(msg['Date'])
if date_tuple:
local_date = dt.datetime.fromtimestamp(
email.utils.mktime_tz(date_tuple))
print ("Local Date:", \
local_date.strftime("%a, %d %b %Y %H:%M:%S"))
def process_message(M, msg_index):
rv, data = M.fetch(msg_index.encode('ascii'), '(RFC822)')
if rv != 'OK':
print("ERROR getting message", rv, file=log_file)
return
msg = email.message_from_bytes(data[0][1])
return Participation(msg)
def process_registrations(M):
rv, msgs = M.search(None, "SUBJECT", SUBJECT_PATTERN, "UNSEEN")
participations = []
if rv != "OK":
print("ERROR searching messages", rv, file=log_file)
return participations
msgs = msgs[0].decode('ascii').split()
if len(msgs) > 0:
print("\tFound %i new registration mails" % len(msgs), file=log_file)
for msg_index in msgs:
participations.append(process_message(M, msg_index))
return participations
def send_confirmation(p=None, csv_file=None):
if p is not None:
assert(isinstance(p, Participation))
content = "Dear %s,\n\n" % (p.name if p else "participant")
with open("response.txt") as fp:
content += fp.read()
content += "\n"
content += str(p) if p else ""
msg = EmailMessage()
msg.set_content(content)
msg['Subject'] = 'Etho 2020 registration confirmation'
msg['From'] = EMAIL_ADDRESS
msg['To'] = p._email if p else EMAIL_ADDRESS
msg['Cc'] = EMAIL_ADDRESS
msg['Reply-to'] = EMAIL_REPLY
s = smtplib.SMTP(SMTP_SERVER)
s.starttls()
s.login(EMAIL_ACCOUNT, EMAIL_PSWD)
s.send_message(msg)
if csv_file and p:
csv_msg = EmailMessage()
csv_msg['Subject'] = 'New Registration ' + p.name if p else ""
csv_msg['From'] = EMAIL_ADDRESS
csv_msg['To'] = EMAIL_REPLY
content = ""
if p and not p.valid:
content += "Registration contains inconsistencies!\n\t"
content += p.inconsistencies
content += "\n\n"
if p and len(p.message) > 0:
content += "Participant left a message:\n %s \n\n" % p.message
if "_" in csv_file:
content += "This seems to be a duplication or correction of an existing registration!\n"
csv_msg.set_content(content)
with open(csv_file, 'rb') as fp:
file_data = fp.read()
csv_msg.add_attachment(file_data, maintype='text', subtype="comma-separated-values",
filename=os.path.basename(csv_file))
s.send_message(csv_msg)
s.quit()
def check_for_mails():
M = imaplib.IMAP4_SSL(IMAP_SERVER)
try:
rv, data = M.login(EMAIL_ACCOUNT, EMAIL_PSWD)
except imaplib.IMAP4.error:
print("LOGIN FAILED!!! ", file=log_file)
return
# sys.exit(1)
# rv, mailboxes = M.list()
# if rv == 'OK':
# print("Mailboxes:")
# print(mailboxes)
rv, data = M.select(EMAIL_FOLDER)
if rv != 'OK':
print("ERROR: Unable to open mailbox folder ", rv, file=log_file)
return
new_participations = process_registrations(M)
for p in new_participations:
print("%s\tNew registration by %s" % (dt.datetime.now().isoformat(), p.name), file=log_file)
csv_file = p.save_to_csv()
send_confirmation(p, csv_file)
M.close()
M.logout()
if __name__ == "__main__":
with open(LOGFILE_NAME, 'a', buffering=1) as log_file:
count = 0
while True:
check_for_mails()
time.sleep(CHECK_INTERVAL)
count +=1
if count % LOG_STATUS_INTERVAL == 0:
print("%s\t Still awake and waiting for registrations!" % (dt.datetime.now().isoformat()), file=log_file)