360 lines
13 KiB
Python
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)
|
|
|