add imap_ssl support, some style fixes

This commit is contained in:
ktims committed 2026-10-18 14:18:05 -07:00
1 parent d414d0cf09
commit 43d9a69707
2 files changed
+54 -42

No files matched your search

+26 -22
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
import asyncio
import email.policy
import importlib.metadata
@@ -6,8 +5,7 @@ import importlib.util
import logging
import os
import ssl
from imaplib import IMAP4
from logging import debug, error, info, warning
from imaplib import IMAP4, IMAP4_SSL
from os import getenv
from pydantic import Field, SecretStr
@@ -20,6 +18,8 @@ CONFIG_PATH = getenv("CONFIG_PATH", "/data/config.py")
ACTUAL_PATH = "./cli.js"
TIMEOUT = 30
log = logging.getLogger("watch")
async def ticker(interval: float):
while True:
@@ -44,6 +44,7 @@ class AppConfig(BaseSettings):
imap_mailbox: str = Field(default="INBOX")
imap_interval: int = Field(default=300)
imap_starttls: bool = Field(default=True)
imap_tls: bool = Field(default_factory=lambda data: data["imap_port"] == 993)
model_config = {"env_prefix": ""}
@@ -63,7 +64,7 @@ class App:
+ f' -d "{t.date}"'
+ (f' -n "{t.notes}"' if t.notes else "")
)
debug("Actual command: %s", cmd)
log.debug("Actual command: %s", cmd)
proc = await asyncio.create_subprocess_shell(
cmd=cmd,
env=os.environ,
@@ -72,12 +73,12 @@ class App:
)
stdout, _stderr = await proc.communicate()
if proc.returncode != 0:
error("Submitting to actual failed: %s", stdout)
log.error("Submitting to actual failed: %s", stdout)
async def process_message(self, msg_b: bytes):
debug("parsing message")
log.debug("parsing message")
msg = email.message_from_bytes(msg_b, policy=email.policy.default)
info(
log.info(
"Found message from %s to %s subject %s",
msg.get("From", "<unknown>"),
msg.get("To", "<unknown>"),
@@ -85,24 +86,27 @@ class App:
)
for parser in self._parsers:
debug("Running parser %s", type(parser).__name__)
log.debug("Running parser %s", type(parser).__name__)
try:
if parser.match(msg):
info("Parser %s claimed message", type(parser).__name__)
log.info("Parser %s claimed message", type(parser).__name__)
trans = parser.extract(msg)
if trans is not None:
info("Submitting transaction to Actual: %s", trans)
log.info("Submitting transaction to Actual: %s", trans)
await self.submit_transaction(trans)
else:
warning("Parser %s returned None", type(parser).__name__)
log.warning("Parser %s returned None", type(parser).__name__)
except TransactionParsingFailed as e:
warning("Unable to parse message %s", e)
except Exception as e:
warning("Unexpected exception %s", e)
log.warning("Unable to parse message %s", e)
except Exception as e: # noqa: BLE001 - trap all errors in the parsers
log.warning("Unexpected exception %s", e)
async def poll_imap(self):
info("polling mailbox")
with IMAP4(self._config.imap_server, self._config.imap_port, TIMEOUT) as M:
log.info("polling mailbox")
imap_class = IMAP4_SSL if self._config.imap_tls else IMAP4
with imap_class(
self._config.imap_server, self._config.imap_port, timeout=TIMEOUT
) as M:
if self._config.imap_starttls:
context = ssl.create_default_context()
M.starttls(context)
@@ -111,16 +115,16 @@ class App:
_status, m_set = M.search(None, "UNSEEN")
for msg_id in m_set[0].split():
debug("Retrieving msg id %s", msg_id)
log.debug("Retrieving msg id %s", msg_id)
_status, msg = M.fetch(msg_id, "(RFC822)")
if _status != "OK" or msg[0] is None:
error("Unable to fetch message %s", msg_id)
log.error("Unable to fetch message %s", msg_id)
continue
msg_body = msg[0][1]
if isinstance(msg_body, int):
error("Unable to fetch message %s", msg_id)
log.error("Unable to fetch message %s", msg_id)
continue
debug("Processing message %s", msg_id)
log.debug("Processing message %s", msg_id)
await self.process_message(msg_body)
async def run(self):
@@ -130,11 +134,11 @@ class App:
def main():
logging.basicConfig(level=getenv("LOG_LEVEL", "INFO"))
logging.info(
log.info(
"actual-imap-parser %s starting up",
importlib.metadata.version("actual-imap-poll"),
)
app_config = AppConfig()
logging.debug("Config: %s", app_config)
log.debug("Config: %s", app_config)
app = App(app_config, CONFIG_PATH)
asyncio.run(app.run())