import os import sys from smtplib import SMTP_SSL import email import datetime from imapclient import IMAPClient, SEEN from dateutil.parser import parse from croniter import croniter IMAP_USER = os.environ["REMINDME_IMAP_USER"] IMAP_PASS = os.environ["REMINDME_IMAP_PASS"] IMAP_HOST = os.environ["REMINDME_IMAP_HOST"] SMTP_USER = os.environ.get("REMINDME_SMTP_USER", IMAP_USER) SMTP_PASS = os.environ.get("REMINDME_IMAP_PASS", IMAP_PASS) SMTP_HOST = os.environ.get("REMINDME_IMAP_HOST", IMAP_HOST) USAGE = "" __iclient = None def iclient(): global __iclient if not __iclient: __iclient = IMAPClient(IMAP_HOST, use_uid=True) __iclient.login(IMAP_USER, IMAP_PASS) __iclient.select_folder("INBOX") return __iclient def make_reply(orig, subject, body): reply = email.message.Message() reply["Subject"] = subject reply["In-Reply-To"] = orig["Message-ID"] reply["References"] = orig["Message-ID"] reply.set_payload(body) reply["From"] = "remindme@friedersdorff.com" reply["To"] = orig["From"] return reply def ls(msg, reminders): reply_lines = [] for uid, reminder in reminders: body = reminder.get_payload().strip() reply_lines.append(f"({uid}) {reminder['subject']}: {body}") return make_reply(msg, "Your current reminders", "\n".join(reply_lines)) def ack_reminder(uid, reminder): body = reminder.get_payload().strip() reply_body = f"({uid}) {reminder['subject']}: {body}" return make_reply(reminder, f"Received Reminder: {reminder['subject']}", reply_body) def remind(last_time, start_time, reminder): f_line = reminder.get_payload().strip().splitlines()[0] if f_line.lower().startswith("repeat"): cron_payload = f_line[6:].strip() it = croniter(cron_payload, last_time) next_time = it.get_next(datetime.datetime) if next_time > last_time and next_time <= start_time: return ( True, make_reply( reminder, f"Re: {reminder['subject']} now", reminder.get_payload() ), ) else: return False, None else: try: parsed_time = parse(f_line, fuzzy=True) except ValueError: return False, None if not parsed_time.tzinfo: send_date = email.utils.parsedate_to_datetime(reminder["Date"]) sender_tz = send_date.tzinfo or datetime.timezone.utc parsed_time = parsed_time.replace(tzinfo=sender_tz) if datetime.datetime.now(datetime.timezone.utc) > parsed_time: return ( True, make_reply( reminder, f"Re: {reminder['subject']} now", reminder.get_payload() ), ) def main(last_time, start_time): msgs = iclient().search(["ALL"]) new_rems, old_rems, metas, to_delete, to_send = [], [], [], [], [] for uid, data in iclient().fetch(msgs, ["BODY.PEEK[]", "FLAGS"]).items(): mail = email.message_from_bytes(data[b"BODY[]"]) if mail.is_multipart(): to_delete.append(uid) payload = mail.get_payload().strip() if payload.lower().startswith("help") or payload.lower().startswith("list"): metas.append(mail) to_delete.append(uid) else: if SEEN in data[b"FLAGS"]: old_rems.append((uid, mail)) else: new_rems.append((uid, mail)) for meta in metas: if meta.get_payload().strip().startswith("help"): to_send.append(make_reply(meta, "Remindme Help", USAGE)) elif meta.get_payload().strip().startswith("list"): to_send.append(ls(meta, old_rems + new_rems)) for uid, reminder in new_rems: to_send.append(ack_reminder(uid, reminder)) for uid, reminder in old_rems + new_rems: done, reply = remind(last_time, start_time, reminder) if reply: to_send.append(reply) if done: to_delete.append(uid) iclient().delete_messages(to_delete) iclient().expunge() iclient().add_flags([u[0] for u in new_rems], SEEN) with SMTP_SSL(SMTP_HOST) as smtp: smtp.login(SMTP_USER, SMTP_PASS) for reply in to_send: smtp.send_message(reply) if __name__ == "__main__": last_time = datetime.datetime.fromtimestamp( float(sys.argv[1]), datetime.timezone.utc ) start_time = datetime.datetime.now(datetime.timezone.utc) main(last_time, start_time) print(start_time.timestamp())