diff --git a/.env.example b/.env.example index 017b0e2..1e4c482 100644 --- a/.env.example +++ b/.env.example @@ -54,3 +54,14 @@ OIDC_USE_DISCOVERY=true YNAB_ACCESS_TOKEN=your-ynab-personal-access-token # Optional: Specify a budget ID, or leave empty to use the default/first budget YNAB_BUDGET_ID= + +# Mealie Configuration +# Base URL for your Mealie instance (e.g., http://192.168.1.5:9000 or https://mealie.example.com) +MEALIE_BASE_URL=http://192.168.1.5:9000 +# Get your API token from Mealie's user settings page +MEALIE_API_TOKEN=your-mealie-api-token + +# Email Integration +# Email Encryption Key (32-byte URL-safe base64) +# Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +FERNET_KEY=your-fernet-key-here diff --git a/aerich_config.py b/aerich_config.py index bfacaa9..d194c65 100644 --- a/aerich_config.py +++ b/aerich_config.py @@ -16,6 +16,7 @@ TORTOISE_ORM = { "models": [ "blueprints.conversation.models", "blueprints.users.models", + "blueprints.email.models", "aerich.models", ], "default_connection": "default", diff --git a/app.py b/app.py index a8369f8..50a3893 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,5 @@ import os +import logging from dotenv import load_dotenv from quart import Quart, jsonify, render_template, request, send_from_directory @@ -7,6 +8,7 @@ from tortoise.contrib.quart import register_tortoise import blueprints.conversation import blueprints.conversation.logic +import blueprints.email import blueprints.rag import blueprints.users import blueprints.users.models @@ -15,6 +17,18 @@ from main import consult_simba_oracle # Load environment variables load_dotenv() +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler()], +) + +# Ensure YNAB and Mealie loggers are visible +logging.getLogger("utils.ynab_service").setLevel(logging.INFO) +logging.getLogger("utils.mealie_service").setLevel(logging.INFO) +logging.getLogger("blueprints.conversation.agents").setLevel(logging.INFO) + app = Quart( __name__, static_folder="raggr-frontend/dist/static", @@ -27,6 +41,7 @@ jwt = JWTManager(app) # Register blueprints app.register_blueprint(blueprints.users.user_blueprint) app.register_blueprint(blueprints.conversation.conversation_blueprint) +app.register_blueprint(blueprints.email.email_blueprint) app.register_blueprint(blueprints.rag.rag_blueprint) @@ -42,6 +57,7 @@ TORTOISE_CONFIG = { "models": [ "blueprints.conversation.models", "blueprints.users.models", + "blueprints.email.models", "aerich.models", ] }, diff --git a/migrations/models/2_20260208091453_add_email_tables.py b/migrations/models/2_20260208091453_add_email_tables.py new file mode 100644 index 0000000..2b5ca90 --- /dev/null +++ b/migrations/models/2_20260208091453_add_email_tables.py @@ -0,0 +1,56 @@ +from tortoise import BaseDBAsyncClient + +RUN_IN_TRANSACTION = True + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + CREATE TABLE IF NOT EXISTS "email_accounts" ( + "id" UUID NOT NULL PRIMARY KEY, + "email_address" VARCHAR(255) NOT NULL UNIQUE, + "display_name" VARCHAR(255), + "imap_host" VARCHAR(255) NOT NULL, + "imap_port" INT NOT NULL DEFAULT 993, + "imap_username" VARCHAR(255) NOT NULL, + "imap_password" TEXT NOT NULL, + "is_active" BOOL NOT NULL DEFAULT TRUE, + "last_error" TEXT, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE TABLE IF NOT EXISTS "email_sync_status" ( + "id" UUID NOT NULL PRIMARY KEY, + "last_sync_date" TIMESTAMPTZ, + "last_message_uid" INT NOT NULL DEFAULT 0, + "message_count" INT NOT NULL DEFAULT 0, + "consecutive_failures" INT NOT NULL DEFAULT 0, + "last_failure_date" TIMESTAMPTZ, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "account_id" UUID NOT NULL UNIQUE REFERENCES "email_accounts" ("id") ON DELETE CASCADE +); +CREATE TABLE IF NOT EXISTS "emails" ( + "id" UUID NOT NULL PRIMARY KEY, + "message_id" VARCHAR(255) NOT NULL UNIQUE, + "subject" VARCHAR(500) NOT NULL, + "from_address" VARCHAR(255) NOT NULL, + "to_address" TEXT NOT NULL, + "date" TIMESTAMPTZ NOT NULL, + "body_text" TEXT, + "body_html" TEXT, + "chromadb_doc_id" VARCHAR(255), + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expires_at" TIMESTAMPTZ NOT NULL, + "account_id" UUID NOT NULL REFERENCES "email_accounts" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_emails_message_9e3c0c" ON "emails" ("message_id");""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + DROP TABLE IF EXISTS "emails"; + DROP TABLE IF EXISTS "email_sync_status"; + DROP TABLE IF EXISTS "email_accounts";""" + + +MODELS_STATE = ""