email-to-calendar 20250826010803.dev0__py3-none-any.whl → 20251210163203.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- __init__.py +10 -12
- db.py +7 -8
- email_to_calendar-20251210163203.dev0.dist-info/METADATA +41 -0
- email_to_calendar-20251210163203.dev0.dist-info/RECORD +20 -0
- events/caldav.py +13 -3
- mail/mail_idle.py +187 -0
- main.py +120 -39
- model/email.py +61 -30
- model/event.py +63 -102
- util/ai.py +163 -0
- util/env.py +97 -0
- util/notifications.py +20 -0
- email_to_calendar-20250826010803.dev0.dist-info/METADATA +0 -25
- email_to_calendar-20250826010803.dev0.dist-info/RECORD +0 -17
- util/text.py +0 -856
- {email_to_calendar-20250826010803.dev0.dist-info → email_to_calendar-20251210163203.dev0.dist-info}/WHEEL +0 -0
- {email_to_calendar-20250826010803.dev0.dist-info → email_to_calendar-20251210163203.dev0.dist-info}/licenses/LICENSE +0 -0
- {email_to_calendar-20250826010803.dev0.dist-info → email_to_calendar-20251210163203.dev0.dist-info}/top_level.txt +0 -0
__init__.py
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
3
|
import logging
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
os.makedirs("../data")
|
|
6
|
+
from src.util.env import get_settings
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
settings = get_settings()
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
os.makedirs("../logs")
|
|
10
|
+
db_file = Path(settings.DB_FILE)
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
db_file.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
|
|
14
|
+
log_file_path = Path(os.path.dirname(__file__), "../logs/emails.log")
|
|
15
|
+
log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
log_file_path.touch(exist_ok=True)
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class StdoutFilter(logging.Filter):
|
|
@@ -43,7 +46,7 @@ stderr_handler.addFilter(StderrFilter())
|
|
|
43
46
|
stderr_handler.setFormatter(logging.Formatter(log_format))
|
|
44
47
|
|
|
45
48
|
# Handler for file (all levels)
|
|
46
|
-
file_handler = logging.FileHandler(
|
|
49
|
+
file_handler = logging.FileHandler(log_file_path)
|
|
47
50
|
file_handler.setLevel(logging.DEBUG)
|
|
48
51
|
file_handler.setFormatter(logging.Formatter(log_format))
|
|
49
52
|
|
|
@@ -52,8 +55,3 @@ logger.handlers.clear()
|
|
|
52
55
|
logger.addHandler(stdout_handler)
|
|
53
56
|
logger.addHandler(stderr_handler)
|
|
54
57
|
logger.addHandler(file_handler)
|
|
55
|
-
|
|
56
|
-
# Usage: from src import logger
|
|
57
|
-
# logger.info("Info message")
|
|
58
|
-
# logger.warning("Warning message")
|
|
59
|
-
# logger.error("Error message")
|
db.py
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
from
|
|
2
|
-
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
1
|
+
from sqlmodel import create_engine, Session
|
|
3
2
|
|
|
4
|
-
from src import
|
|
3
|
+
from src.util.env import get_settings
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
settings = get_settings()
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
DATABASE_URL = f"sqlite:///{settings.DB_FILE}"
|
|
8
|
+
|
|
9
|
+
engine = create_engine(DATABASE_URL)
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
def get_db():
|
|
14
|
-
db =
|
|
13
|
+
db = Session(engine)
|
|
15
14
|
try:
|
|
16
15
|
yield db
|
|
17
16
|
finally:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: email-to-calendar
|
|
3
|
+
Version: 20251210163203.dev0
|
|
4
|
+
Summary: Takes emails from an IMAP server, parses the body, and creates event(s) in a CalDAV calendar
|
|
5
|
+
Requires-Python: <4.0,>=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: apprise==1.9.6
|
|
9
|
+
Requires-Dist: caldav==2.2.3
|
|
10
|
+
Requires-Dist: imapclient==3.0.1
|
|
11
|
+
Requires-Dist: pydantic-ai-slim[openai]==1.29.0
|
|
12
|
+
Requires-Dist: pydantic-settings==2.12.0
|
|
13
|
+
Requires-Dist: pydantic[email]==2.12.5
|
|
14
|
+
Requires-Dist: sqlmodel==0.0.27
|
|
15
|
+
Requires-Dist: tzlocal==5.3.1
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# E-Mail to calendar Converter
|
|
19
|
+
The point of this application is to search an IMAP account, look for emails based on certain criteria(s), and parse
|
|
20
|
+
the content, using regex, and automatically create calendar events in an iCal account.
|
|
21
|
+
|
|
22
|
+
## TO-DO
|
|
23
|
+
- [X] Get e-mails, and save ID to sqlite db to avoid duplicates
|
|
24
|
+
- [X] Save calendar events to sqlite db to avoid duplicates
|
|
25
|
+
- [X] Add config to backfill (check all emails from an optional certain date), or use most recent email
|
|
26
|
+
- [X] If using most recent, when new email arrives, remove events not present, and add new ones
|
|
27
|
+
- [ ] If new email comes in with updated events, update event in calendar instead of creating a new one
|
|
28
|
+
- [ ] Using email summary check for words like `Cancelled`, etc. to delete events
|
|
29
|
+
- [ ] If event already exists, check if details have changed, and update if necessary
|
|
30
|
+
- [ ] Investigate IMAP IDLE (push instead of poll)
|
|
31
|
+
- [X] Make sure all day events are handled correctly
|
|
32
|
+
- [ ] Add Docker Model Runner support
|
|
33
|
+
- [ ] Add 'validate' function for events, and if it fails, have AI re-process that event
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Environment Variables
|
|
37
|
+
| Name | Description | Type | Default Value | Allowed Values |
|
|
38
|
+
|------|-------------|------|---------------|----------------|
|
|
39
|
+
| | | | | |
|
|
40
|
+
| | | | | |
|
|
41
|
+
| | | | | |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__init__.py,sha256=iI9fqY9bavUE4S2CxDks8-Q7OBGKl96xQVbYRmAm36E,1572
|
|
2
|
+
db.py,sha256=apQrwaEp0ncFNn5WIoDi7ndMe4Q-VYRnXvCJT_iV0O0,295
|
|
3
|
+
main.py,sha256=CZxXR163RvFC_7C8CKK39JDEOQJlpobK1Mi7y-fUeR8,6308
|
|
4
|
+
email_to_calendar-20251210163203.dev0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
5
|
+
events/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
events/caldav.py,sha256=Tf-v_yuHr6SBzQ4f-AOdKGG_eoIVuedZ5AKCv-hKWG8,1713
|
|
7
|
+
mail/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
mail/mail.py,sha256=Kq50o2kpkovvfNcSnDQZTnOZ6TV7ZSR8KOIRvdXkGao,5825
|
|
9
|
+
mail/mail_idle.py,sha256=o_lOVO9VPm88si3qvEQhttSH2N6cRZF7AWV2kU8uXDw,6860
|
|
10
|
+
model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
model/email.py,sha256=GRpqoOQikfZAWofPOYpHGVIbkNVr3_XahMsorwsagis,3647
|
|
12
|
+
model/event.py,sha256=WQPVKPAuebALqalZYHvl1sAmQ3BPnu6XDDmpvrgNTWM,3917
|
|
13
|
+
util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
util/ai.py,sha256=Px1OU2VLbnXr9Fk7VHj66b0_-hyvrGKsInAmZigwIag,7367
|
|
15
|
+
util/env.py,sha256=gTgydkmnEgOYvltMqt0hlYWErVf08ZUssbhHzA0GCiE,3780
|
|
16
|
+
util/notifications.py,sha256=WKSgBfVeDOg1tqIEOwUgMgb10qxrzjsDvh5bmYi9-Hw,640
|
|
17
|
+
email_to_calendar-20251210163203.dev0.dist-info/METADATA,sha256=RgfWJh0MsADtWqdSEVjo8Z8ZCPznCuppapDq585_k2M,1989
|
|
18
|
+
email_to_calendar-20251210163203.dev0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
+
email_to_calendar-20251210163203.dev0.dist-info/top_level.txt,sha256=635ZTCyzc_2xtdJz_2Q50A96mBoIUrfORy5SdfGYabo,40
|
|
20
|
+
email_to_calendar-20251210163203.dev0.dist-info/RECORD,,
|
events/caldav.py
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
from caldav import DAVClient, Calendar
|
|
2
|
+
from pydantic import AnyUrl
|
|
3
|
+
|
|
2
4
|
from src import logger
|
|
3
5
|
|
|
4
6
|
from src.model.event import Event
|
|
5
7
|
|
|
6
8
|
|
|
7
|
-
def authenticate_caldav(url:
|
|
8
|
-
return DAVClient(
|
|
9
|
+
def authenticate_caldav(url: AnyUrl, username: str, password: str) -> DAVClient:
|
|
10
|
+
return DAVClient(
|
|
11
|
+
url.encoded_string(),
|
|
12
|
+
username=username,
|
|
13
|
+
password=password,
|
|
14
|
+
headers={"User-Agent": "email-to-calendar/1.0"},
|
|
15
|
+
)
|
|
9
16
|
|
|
10
17
|
|
|
11
18
|
def add_to_caldav(
|
|
12
|
-
url:
|
|
19
|
+
url: AnyUrl, username: str, password: str, calendar_name: str, events: list[Event]
|
|
13
20
|
):
|
|
14
21
|
with authenticate_caldav(url, username, password) as client:
|
|
15
22
|
principal = client.principal()
|
|
@@ -30,6 +37,9 @@ def add_to_caldav(
|
|
|
30
37
|
logger.info(
|
|
31
38
|
f"Adding event {event.summary} to CalDAV calendar '{calendar_name}'"
|
|
32
39
|
)
|
|
40
|
+
if event.all_day:
|
|
41
|
+
event.start = event.start.date()
|
|
42
|
+
event.end = event.end.date()
|
|
33
43
|
calendar.add_event(
|
|
34
44
|
dtstart=event.start, dtend=event.end, summary=event.summary
|
|
35
45
|
)
|
mail/mail_idle.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import re
|
|
3
|
+
import email
|
|
4
|
+
from email import policy
|
|
5
|
+
from imapclient import IMAPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def idle_print_emails(
|
|
9
|
+
host,
|
|
10
|
+
port,
|
|
11
|
+
username,
|
|
12
|
+
password,
|
|
13
|
+
folder="INBOX",
|
|
14
|
+
idle_timeout=30,
|
|
15
|
+
payload_timeout=30,
|
|
16
|
+
retry_interval=1,
|
|
17
|
+
):
|
|
18
|
+
"""
|
|
19
|
+
Connects to IMAP server, enters IDLE loop and prints new messages to console.
|
|
20
|
+
Only prints a message once its payload is actually available. Retries fetching
|
|
21
|
+
the payload for up to `payload_timeout` seconds (checking every `retry_interval` seconds).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
processed_uids = set()
|
|
25
|
+
|
|
26
|
+
def extract_text(msg):
|
|
27
|
+
if msg.is_multipart():
|
|
28
|
+
for part in msg.walk():
|
|
29
|
+
if (
|
|
30
|
+
part.get_content_type() == "text/plain"
|
|
31
|
+
and part.get_content_disposition() != "attachment"
|
|
32
|
+
):
|
|
33
|
+
return part.get_content()
|
|
34
|
+
for part in msg.walk():
|
|
35
|
+
if (
|
|
36
|
+
part.get_content_type() == "text/html"
|
|
37
|
+
and part.get_content_disposition() != "attachment"
|
|
38
|
+
):
|
|
39
|
+
return part.get_content()
|
|
40
|
+
return ""
|
|
41
|
+
else:
|
|
42
|
+
return msg.get_content()
|
|
43
|
+
|
|
44
|
+
def _raw_from_fetch_entry(data):
|
|
45
|
+
# fetch response keys vary; try common ones
|
|
46
|
+
for key in (
|
|
47
|
+
b"BODY.PEEK[]",
|
|
48
|
+
"BODY.PEEK[]",
|
|
49
|
+
b"BODY[]",
|
|
50
|
+
"BODY[]",
|
|
51
|
+
b"RFC822",
|
|
52
|
+
"RFC822",
|
|
53
|
+
):
|
|
54
|
+
raw = data.get(key)
|
|
55
|
+
if raw:
|
|
56
|
+
return raw
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def fetch_payload_with_retry(client, uid):
|
|
60
|
+
start = time.time()
|
|
61
|
+
while time.time() - start < payload_timeout:
|
|
62
|
+
try:
|
|
63
|
+
fetch_data = client.fetch([uid], ["BODY.PEEK[]", "FLAGS"])
|
|
64
|
+
except Exception:
|
|
65
|
+
# transient fetch error, wait and retry
|
|
66
|
+
time.sleep(retry_interval)
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
entry = fetch_data.get(uid) or fetch_data.get(int(uid)) or {}
|
|
70
|
+
raw = _raw_from_fetch_entry(entry)
|
|
71
|
+
if raw:
|
|
72
|
+
# normalize
|
|
73
|
+
if isinstance(raw, tuple):
|
|
74
|
+
raw = raw[1]
|
|
75
|
+
if isinstance(raw, str):
|
|
76
|
+
raw = raw.encode()
|
|
77
|
+
return raw
|
|
78
|
+
# not yet present, wait and retry
|
|
79
|
+
time.sleep(retry_interval)
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
def process_uids(client, uids):
|
|
83
|
+
for uid in uids:
|
|
84
|
+
if uid in processed_uids:
|
|
85
|
+
continue
|
|
86
|
+
raw = fetch_payload_with_retry(client, uid)
|
|
87
|
+
if not raw:
|
|
88
|
+
# payload never became available within timeout; skip printing
|
|
89
|
+
print(
|
|
90
|
+
f"UID {uid}: payload not available after {payload_timeout}s, skipping."
|
|
91
|
+
)
|
|
92
|
+
continue
|
|
93
|
+
try:
|
|
94
|
+
msg = email.message_from_bytes(raw, policy=policy.default)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"UID {uid}: failed to parse message bytes:", e)
|
|
97
|
+
processed_uids.add(uid)
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
subject = msg.get("Subject", "(no subject)")
|
|
101
|
+
_from = msg.get("From", "(no from)")
|
|
102
|
+
_to = msg.get("To", "(no to)")
|
|
103
|
+
date = msg.get("Date", "(no date)")
|
|
104
|
+
body = extract_text(msg) or "(no body)"
|
|
105
|
+
|
|
106
|
+
print("----- MESSAGE START -----")
|
|
107
|
+
print("UID:", uid)
|
|
108
|
+
print("From:", _from)
|
|
109
|
+
print("To:", _to)
|
|
110
|
+
print("Date:", date)
|
|
111
|
+
print("Subject:", subject)
|
|
112
|
+
print("Body:\n", body)
|
|
113
|
+
print("----- MESSAGE END -------")
|
|
114
|
+
|
|
115
|
+
processed_uids.add(uid)
|
|
116
|
+
|
|
117
|
+
with IMAPClient(host, port, use_uid=True, ssl=True) as client:
|
|
118
|
+
client.login(username, password)
|
|
119
|
+
client.select_folder(folder)
|
|
120
|
+
print("Connected and monitoring folder:", folder)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
while True:
|
|
124
|
+
print("Entering IDLE...")
|
|
125
|
+
started_idle = False
|
|
126
|
+
try:
|
|
127
|
+
client.idle()
|
|
128
|
+
started_idle = True
|
|
129
|
+
except Exception as e:
|
|
130
|
+
# servers can send unsolicited FETCH while entering IDLE; extract UIDs and process
|
|
131
|
+
msg = str(e)
|
|
132
|
+
print("IDLE start error (handled):", msg)
|
|
133
|
+
found = re.findall(r"UID (\d+)", msg)
|
|
134
|
+
if found:
|
|
135
|
+
uids = [int(x) for x in found]
|
|
136
|
+
print("Processing UIDs from unsolicited response:", uids)
|
|
137
|
+
process_uids(client, uids)
|
|
138
|
+
|
|
139
|
+
responses = []
|
|
140
|
+
if started_idle:
|
|
141
|
+
try:
|
|
142
|
+
responses = client.idle_check(timeout=idle_timeout)
|
|
143
|
+
finally:
|
|
144
|
+
try:
|
|
145
|
+
client.idle_done()
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print("Failed to exit IDLE:", e)
|
|
148
|
+
|
|
149
|
+
if responses:
|
|
150
|
+
print("Server notifications:", responses)
|
|
151
|
+
uids_from_responses = []
|
|
152
|
+
for resp in responses:
|
|
153
|
+
try:
|
|
154
|
+
if (
|
|
155
|
+
isinstance(resp, tuple)
|
|
156
|
+
and len(resp) >= 3
|
|
157
|
+
and resp[1] in (b"FETCH", "FETCH")
|
|
158
|
+
):
|
|
159
|
+
nested = resp[2]
|
|
160
|
+
if isinstance(nested, tuple):
|
|
161
|
+
for i in range(len(nested)):
|
|
162
|
+
if nested[i] == b"UID" or nested[i] == "UID":
|
|
163
|
+
maybe_uid = nested[i + 1]
|
|
164
|
+
uids_from_responses.append(int(maybe_uid))
|
|
165
|
+
except Exception:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
if uids_from_responses:
|
|
169
|
+
process_uids(client, uids_from_responses)
|
|
170
|
+
else:
|
|
171
|
+
# fallback: search for UNSEEN messages
|
|
172
|
+
try:
|
|
173
|
+
uids = client.search(["UNSEEN"])
|
|
174
|
+
if not uids:
|
|
175
|
+
print("No unseen messages found.")
|
|
176
|
+
else:
|
|
177
|
+
print(f"Found {len(uids)} unseen message(s): {uids}")
|
|
178
|
+
process_uids(client, uids)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print("Error fetching or parsing messages:", e)
|
|
181
|
+
|
|
182
|
+
time.sleep(1)
|
|
183
|
+
except KeyboardInterrupt:
|
|
184
|
+
print("Interrupted by user, closing connection.")
|
|
185
|
+
except Exception as e:
|
|
186
|
+
print("Error in IDLE loop:", e)
|
|
187
|
+
raise
|
main.py
CHANGED
|
@@ -1,45 +1,133 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import datetime
|
|
1
3
|
import os
|
|
4
|
+
import time
|
|
2
5
|
|
|
3
6
|
from sqlalchemy import inspect
|
|
7
|
+
from sqlmodel import SQLModel
|
|
4
8
|
|
|
5
9
|
from src import db_file, logger
|
|
6
10
|
from src.events.caldav import add_to_caldav
|
|
7
|
-
from src.model.event import Event
|
|
8
|
-
from src.util import text
|
|
9
11
|
from src.mail import mail
|
|
10
|
-
from src.db import
|
|
12
|
+
from src.db import Session, engine
|
|
11
13
|
from src.model.email import EMail
|
|
14
|
+
from src.model.event import Event
|
|
15
|
+
from src.util.ai import parse_email
|
|
16
|
+
from src.util.env import get_settings, Settings
|
|
17
|
+
from src.util.notifications import send_success_notification
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def populate_events(settings: Settings):
|
|
21
|
+
backfill = settings.BACKFILL
|
|
22
|
+
provider = settings.AI_PROVIDER
|
|
23
|
+
model = settings.AI_MODEL
|
|
24
|
+
ollama_host = settings.OLLAMA_HOST
|
|
25
|
+
ollama_port = settings.OLLAMA_PORT
|
|
26
|
+
ollama_secure = settings.OLLAMA_SECURE
|
|
27
|
+
open_ai_api_key = settings.OPEN_AI_API_KEY
|
|
28
|
+
max_retries = settings.AI_MAX_RETRIES
|
|
29
|
+
system_prompt = settings.AI_SYSTEM_PROMPT
|
|
30
|
+
|
|
31
|
+
if backfill:
|
|
32
|
+
logger.info("Backfilling events from all emails without events")
|
|
33
|
+
for email in EMail.get_without_events():
|
|
34
|
+
events = await parse_email(
|
|
35
|
+
email,
|
|
36
|
+
provider,
|
|
37
|
+
model,
|
|
38
|
+
ollama_host,
|
|
39
|
+
ollama_port,
|
|
40
|
+
ollama_secure,
|
|
41
|
+
open_ai_api_key,
|
|
42
|
+
max_retries,
|
|
43
|
+
system_prompt,
|
|
44
|
+
)
|
|
45
|
+
for event in events:
|
|
46
|
+
logger.debug(f"Backfilling event: {event}")
|
|
47
|
+
try:
|
|
48
|
+
event.save()
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.error(f"Error saving event {event}: {e}", exc_info=True)
|
|
51
|
+
for e in events:
|
|
52
|
+
e.delete()
|
|
53
|
+
logger.info("Backfilled events from all emails")
|
|
54
|
+
else:
|
|
55
|
+
most_recent_email = EMail.get_most_recent_without_events()
|
|
56
|
+
if most_recent_email:
|
|
57
|
+
logger.info("Parsing most recent email with id %s", most_recent_email.id)
|
|
58
|
+
events = await parse_email(
|
|
59
|
+
most_recent_email,
|
|
60
|
+
provider,
|
|
61
|
+
model,
|
|
62
|
+
ollama_host,
|
|
63
|
+
ollama_port,
|
|
64
|
+
ollama_secure,
|
|
65
|
+
open_ai_api_key,
|
|
66
|
+
max_retries,
|
|
67
|
+
system_prompt,
|
|
68
|
+
)
|
|
69
|
+
for event in events:
|
|
70
|
+
logger.debug(f"Saving event: {event}")
|
|
71
|
+
try:
|
|
72
|
+
event.save()
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Error saving event {event}: {e}", exc_info=True)
|
|
75
|
+
for e in events:
|
|
76
|
+
e.delete()
|
|
77
|
+
logger.info(
|
|
78
|
+
"Parsed and saved events from most recent email with date %s",
|
|
79
|
+
most_recent_email.delivery_date,
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
logger.info("No new emails without events to parse")
|
|
83
|
+
|
|
84
|
+
events = Event.get_not_in_calendar()
|
|
85
|
+
|
|
86
|
+
caldav_url = settings.CALDAV_URL
|
|
87
|
+
caldav_username = settings.CALDAV_USERNAME
|
|
88
|
+
caldav_password = settings.CALDAV_PASSWORD
|
|
89
|
+
calendar_name = settings.CALDAV_CALENDAR
|
|
90
|
+
|
|
91
|
+
if events:
|
|
92
|
+
logger.info("Adding %d new events to CalDAV calendar", len(events))
|
|
93
|
+
add_to_caldav(
|
|
94
|
+
caldav_url, caldav_username, caldav_password, calendar_name, events
|
|
95
|
+
)
|
|
96
|
+
if settings.APPRISE_URL:
|
|
97
|
+
send_success_notification(settings.APPRISE_URL, events)
|
|
12
98
|
|
|
13
99
|
|
|
14
100
|
def main():
|
|
15
101
|
logger.info("Starting email retrieval process")
|
|
102
|
+
settings = get_settings()
|
|
16
103
|
|
|
17
104
|
# Create tables if they don't exist
|
|
18
|
-
|
|
105
|
+
SQLModel.metadata.create_all(engine)
|
|
19
106
|
|
|
20
|
-
imap_host =
|
|
21
|
-
imap_port =
|
|
22
|
-
imap_username =
|
|
23
|
-
imap_password =
|
|
107
|
+
imap_host = settings.IMAP_HOST
|
|
108
|
+
imap_port = settings.IMAP_PORT
|
|
109
|
+
imap_username = settings.IMAP_USERNAME
|
|
110
|
+
imap_password = settings.IMAP_PASSWORD
|
|
111
|
+
mailbox = settings.IMAP_MAILBOX
|
|
112
|
+
ssl = settings.IMAP_SSL
|
|
24
113
|
|
|
25
|
-
from_email =
|
|
26
|
-
subject =
|
|
27
|
-
backfill: bool = os.environ.get("BACKFILL", "false").lower() == "true"
|
|
114
|
+
from_email = settings.FILTER_FROM_EMAIL
|
|
115
|
+
subject = settings.FILTER_SUBJECT
|
|
28
116
|
|
|
29
117
|
db_path = os.path.join(os.path.dirname(__file__), db_file)
|
|
30
118
|
db_exists = os.path.exists(db_path)
|
|
31
119
|
inspector = inspect(engine)
|
|
32
|
-
table_exists = inspector.has_table("
|
|
120
|
+
table_exists = inspector.has_table("email")
|
|
33
121
|
has_record = False
|
|
34
122
|
if db_exists and table_exists:
|
|
35
123
|
logger.info("Database and table exist, checking for records")
|
|
36
|
-
session =
|
|
124
|
+
session = Session(engine)
|
|
37
125
|
try:
|
|
38
126
|
has_record = len(EMail.get_all()) > 0
|
|
39
127
|
finally:
|
|
40
128
|
session.close()
|
|
41
129
|
|
|
42
|
-
client = mail.authenticate(imap_host, imap_port, imap_username, imap_password)
|
|
130
|
+
client = mail.authenticate(imap_host, imap_port, imap_username, imap_password, ssl)
|
|
43
131
|
|
|
44
132
|
try:
|
|
45
133
|
if has_record:
|
|
@@ -55,6 +143,7 @@ def main():
|
|
|
55
143
|
from_email=from_email,
|
|
56
144
|
subject=subject,
|
|
57
145
|
since=most_recent_email.delivery_date,
|
|
146
|
+
mailbox=mailbox,
|
|
58
147
|
)
|
|
59
148
|
else:
|
|
60
149
|
logger.info("No existing records found, retrieving all emails")
|
|
@@ -67,31 +156,23 @@ def main():
|
|
|
67
156
|
for email in emails:
|
|
68
157
|
email.save()
|
|
69
158
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
caldav_url = os.environ["CALDAV_URL"]
|
|
88
|
-
caldav_username = os.environ["CALDAV_USERNAME"]
|
|
89
|
-
caldav_password = os.environ["CALDAV_PASSWORD"]
|
|
90
|
-
calendar_name = os.environ["CALDAV_CALENDAR"]
|
|
91
|
-
|
|
92
|
-
add_to_caldav(
|
|
93
|
-
caldav_url, caldav_username, caldav_password, calendar_name, events
|
|
94
|
-
)
|
|
159
|
+
while True:
|
|
160
|
+
logger.info("Starting event population process...")
|
|
161
|
+
start_time = datetime.datetime.now()
|
|
162
|
+
try:
|
|
163
|
+
asyncio.run(populate_events(settings))
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.error("Error populating events: %s", e, e)
|
|
166
|
+
finally:
|
|
167
|
+
end_time = datetime.datetime.now()
|
|
168
|
+
duration = (end_time - start_time).total_seconds()
|
|
169
|
+
logger.info(
|
|
170
|
+
"Event population process completed in %.2f seconds", duration
|
|
171
|
+
)
|
|
172
|
+
sleep_duration = settings.INTERVAL_MINUTES * 60
|
|
173
|
+
remaining_time = max(0, sleep_duration - int(duration))
|
|
174
|
+
logger.info("Sleeping for %d seconds", remaining_time)
|
|
175
|
+
time.sleep(remaining_time)
|
|
95
176
|
|
|
96
177
|
except Exception as e:
|
|
97
178
|
logger.error("An error occurred while retrieving emails: %s", e)
|