diff --git a/README.md b/README.md new file mode 100644 index 0000000..de397bd --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Actual IMAP Poll + +This project is designed to poll an IMAP server, looking for transaction notification emails which are pushed to an Actual Budget instance. + +## Docker Usage + +To run this project using Docker, you need to configure the following environment variables: + +### Environment Variables + +| Variable Name | Description | Example Value | +| ----------------- | --------------------------------------------------------------------------------------- | -------------------------------------- | +| `IMAP_SERVER` | The hostname or IP address of the IMAP server. | `imap.example.com` | +| `IMAP_PORT` | The port number for the IMAP server. | `143` | +| `IMAP_USER` | The username for authenticating with the IMAP server. | `user@example.com` | +| `IMAP_PASS` | The password for authenticating with the IMAP server. | `yourpassword` | +| `IMAP_STARTTLS` | Whether to use TLS for the IMAP connection (`true` or `false`). | `true` | +| `IMAP_INTERVAL` | The interval (in seconds) between polling the IMAP server. | `300` | +| `ACTUAL_SERVER` | The URL to the Actual Budget instance to submit transactions to | `https://demo.actualbudget.org` | +| `ACTUAL_PASSWORD` | The password for the Actual Budget instance | `password` | +| `ACTUAL_SYNC_ID` | The 'sync ID' UUID of the Actual Budget budget on the server (see Settings / Advanced) | `145b1875-8d2c-4eac-aa2c-b3fd937c6a6d` | +| `CONFIG_PATH` | Path to a Python module containing a description of the parsers to execute (see below). | `/data/config.py` | + +## Parser Definition + +The parsers to execute and their parameters are defined in a Python source file which should be mounted to the container at the location specified by `CONFIG_PATH` (e.g. `/data/config.py`). This file should define a list called `PARSERS` which holds a list of `TransactionParser` instances. Since it is Python source, it can also contain custom `TransactionParser` instances. + +Each parser instance will generally require the account UUID to link the transactions to, which can be obtained from the end of the URL while viewing the account in Actual. + +### Example config.py +```python config.py +from parsers import RogersBankParser, MBNAParser +from uuid import UUID + +PARSERS = [ + RogersBankParser(UUID("ae662c3c-3f36-4aee-a558-aa32ff7b75c8")), + MBNAParser(UUID("82a382e6-ff27-49af-841f-0cbddee9fe28")), +] +``` + +## Custom Parser Implementation + +Parsers must implement the abstract class `TransactionParser` defined in `parsers.py`. This class requires two methods: + +1. `match(self, msg: Message) -> bool` accepts a `Message/EmailMessage` object and uses it to determine if this parser is responsible for the message and it contains an interesting transaction +2. `extract(self, msg: EmailMessage) -> Transaction` accepts an `EmailMessage` object and extracts a `Transaction` dataclass object for submission to Actual + +### Example custom parser + +Below is a minimal example of a custom parser for a fictional bank, "Minimal Bank." This parser matches emails from "Minimal Bank" and extracts transaction details. It should appear in the `config.py` alongside the `PARSERS` definition. + +```python +class MinimalBankParser(TransactionParser): + def __init__(self, account_id): + self.account_id = account_id # Actual account UUID to submit the matching transactions to + + def match(self, msg): + return msg["From"] == "Minimal Bank " and "Transaction Alert" in msg["Subject"] + + def extract(self, msg): + body = msg.get_body().as_string() + amount = ... # Extract amount from body + date = ... # Extract date from body + payee = ... # Extract payee from body + return Transaction( + account=self.account_id, + date=date, + amount=amount, + payee=payee, + notes="via Minimal Bank email", + imported_id=msg["Message-ID"], + ) +``` + +### Example Docker Command + +```bash +docker run -d \ + -e IMAP_SERVER=imap.example.com \ + -e IMAP_USER=user@example.com \ + -e IMAP_PASS=yourpassword \ + -e ACTUAL_SERVER=https://demo.actualbudget.org \ + -e ACTUAL_PASSWORD=password \ + -e ACTUAL_SYNC_ID=145b1875-8d2c-4eac-aa2c-b3fd937c6a6d \ + -v /path/to/config.py:/data/config.py \ + actual-imap-poll:latest +``` diff --git a/model.py b/model.py index 79a1897..b07a0d0 100644 --- a/model.py +++ b/model.py @@ -6,9 +6,14 @@ from uuid import UUID @dataclass class Transaction: + """Represents an Actual Budget transaction to be submitted. + + See: https://actualbudget.org/docs/api/reference#transaction for field descriptions + """ + account: UUID date: date - amount: Decimal + amount: Decimal # Note: decimal dollars, JS shim will convert to cents as described in the API payee: str # imported_payee in API notes: str imported_id: str diff --git a/parsers.py b/parsers.py index d4d8553..f9ae240 100644 --- a/parsers.py +++ b/parsers.py @@ -13,10 +13,31 @@ from model import Transaction class TransactionParser(ABC): @abstractmethod def match(self, msg: Message) -> bool: + """ + Determines if the given email message matches the criteria for this parser. + + Args: + msg (Message): The email message to evaluate. + + Returns: + bool: True if the message matches the parser's criteria, False otherwise. + """ pass @abstractmethod def extract(self, msg: EmailMessage) -> Transaction: + """ + Extracts transaction details from the given email message. + + Args: + msg (EmailMessage): The email message to parse. + + Returns: + Transaction: A Transaction object containing the extracted details. + + Raises: + TransactionParsingFailed: If the message cannot be parsed successfully. + """ pass