]> git.friedersdorff.com Git - max/remindme.git/blob - remindme.py
Add basic Dockerfile for this app
[max/remindme.git] / remindme.py
1 import os
2 import sys
3 from smtplib import SMTP_SSL
4 import email
5 import datetime
6
7 from imapclient import IMAPClient, SEEN
8 from dateutil.parser import parse
9 from croniter import croniter
10
11
12 IMAP_USER = os.environ["REMINDME_IMAP_USER"]
13 IMAP_PASS = os.environ["REMINDME_IMAP_PASS"]
14 IMAP_HOST = os.environ["REMINDME_IMAP_HOST"]
15 SMTP_USER = os.environ.get("REMINDME_SMTP_USER", IMAP_USER)
16 SMTP_PASS = os.environ.get("REMINDME_IMAP_PASS", IMAP_PASS)
17 SMTP_HOST = os.environ.get("REMINDME_IMAP_HOST", IMAP_HOST)
18
19 USAGE = ""
20
21 __iclient = None
22
23
24 def iclient():
25     global __iclient
26     if not __iclient:
27         __iclient = IMAPClient(IMAP_HOST, use_uid=True)
28         __iclient.login(IMAP_USER, IMAP_PASS)
29
30     __iclient.select_folder("INBOX")
31     return __iclient
32
33
34 def make_reply(orig, subject, body):
35     reply = email.message.Message()
36     reply["Subject"] = subject
37     reply["In-Reply-To"] = orig["Message-ID"]
38     reply["References"] = orig["Message-ID"]
39     reply.set_payload(body)
40     reply["From"] = "remindme@friedersdorff.com"
41     reply["To"] = orig["From"]
42     return reply
43
44
45 def ls(msg, reminders):
46     reply_lines = []
47     for uid, reminder in reminders:
48         body = reminder.get_payload().strip()
49         reply_lines.append(f"({uid}) {reminder['subject']}: {body}")
50
51     return make_reply(msg, "Your current reminders", "\n".join(reply_lines))
52
53
54 def ack_reminder(uid, reminder):
55     body = reminder.get_payload().strip()
56     reply_body = f"({uid}) {reminder['subject']}: {body}"
57     return make_reply(reminder, f"Received Reminder: {reminder['subject']}", reply_body)
58
59
60 def remind(last_time, start_time, reminder):
61     f_line = reminder.get_payload().strip().splitlines()[0]
62     if f_line.lower().startswith("repeat"):
63         cron_payload = f_line[6:].strip()
64         it = croniter(cron_payload, last_time)
65         next_time = it.get_next(datetime.datetime)
66         if next_time > last_time and next_time <= start_time:
67             return (
68                 True,
69                 make_reply(
70                     reminder, f"Re: {reminder['subject']} now", reminder.get_payload()
71                 ),
72             )
73         else:
74             return False, None
75     else:
76         try:
77             parsed_time = parse(f_line, fuzzy=True)
78         except ValueError:
79             return False, None
80
81         if not parsed_time.tzinfo:
82             send_date = email.utils.parsedate_to_datetime(reminder["Date"])
83             sender_tz = send_date.tzinfo or datetime.timezone.utc
84             parsed_time = parsed_time.replace(tzinfo=sender_tz)
85
86         if datetime.datetime.now(datetime.timezone.utc) > parsed_time:
87             return (
88                 True,
89                 make_reply(
90                     reminder, f"Re: {reminder['subject']} now", reminder.get_payload()
91                 ),
92             )
93
94
95 def main(last_time, start_time):
96     msgs = iclient().search(["ALL"])
97     new_rems, old_rems, metas, to_delete, to_send = [], [], [], [], []
98     for uid, data in iclient().fetch(msgs, ["BODY.PEEK[]", "FLAGS"]).items():
99         mail = email.message_from_bytes(data[b"BODY[]"])
100         if mail.is_multipart():
101             to_delete.append(uid)
102
103         payload = mail.get_payload().strip()
104         if payload.lower().startswith("help") or payload.lower().startswith("list"):
105             metas.append(mail)
106             to_delete.append(uid)
107         else:
108             if SEEN in data[b"FLAGS"]:
109                 old_rems.append((uid, mail))
110             else:
111                 new_rems.append((uid, mail))
112
113     for meta in metas:
114         if meta.get_payload().strip().startswith("help"):
115             to_send.append(make_reply(meta, "Remindme Help", USAGE))
116         elif meta.get_payload().strip().startswith("list"):
117             to_send.append(ls(meta, old_rems + new_rems))
118
119     for uid, reminder in new_rems:
120         to_send.append(ack_reminder(uid, reminder))
121
122     for uid, reminder in old_rems + new_rems:
123         done, reply = remind(last_time, start_time, reminder)
124         if reply:
125             to_send.append(reply)
126         if done:
127             to_delete.append(uid)
128
129     iclient().delete_messages(to_delete)
130     iclient().expunge()
131     iclient().add_flags([u[0] for u in new_rems], SEEN)
132
133     with SMTP_SSL(SMTP_HOST) as smtp:
134         smtp.login(SMTP_USER, SMTP_PASS)
135         for reply in to_send:
136             smtp.send_message(reply)
137
138
139 if __name__ == "__main__":
140     last_time = datetime.datetime.fromtimestamp(
141         float(sys.argv[1]), datetime.timezone.utc
142     )
143     start_time = datetime.datetime.now(datetime.timezone.utc)
144     main(last_time, start_time)
145     print(start_time.timestamp())