#!/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)