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