import smtplib import subprocess import json import os from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from datetime import date, timedelta import calendar as cal_module from bills import load_bills from settings import get_setting def _gmail(): """Resolve Gmail creds + recipient from Settings (env-var fallback).""" email = get_setting("gmail_address", env="GMAIL_ADDRESS", default="") pw = get_setting("gmail_app_password", env="GMAIL_APP_PASSWORD", default="") recipient = get_setting("reminder_recipient", default="") or email return email, pw, recipient def send_email(subject, body_html, body_text): email, pw, recipient = _gmail() if not (email and pw): print("Gmail not configured in Settings โ€” skipping email reminder.") return msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = f"Moon Household Budget <{email}>" msg["To"] = recipient msg.attach(MIMEText(body_text, "plain")) msg.attach(MIMEText(body_html, "html")) with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server: server.login(email, pw) server.sendmail(email, recipient, msg.as_string()) def mac_notification(title, message): # macOS-only; no-op (don't crash) when osascript isn't available, e.g. in # the Linux container. script = f'display notification "{message}" with title "{title}" sound name "Default"' try: subprocess.run(["osascript", "-e", script], check=False) except (FileNotFoundError, OSError): pass def bills_due_in(days): today = date.today() target = today + timedelta(days=days) all_bills = load_bills() due = [] for b in all_bills: if not b.get("active", True): continue if b.get("autopay", False): continue if not b.get("remind", True): continue due_day = b.get("due_day") if not due_day: continue _, num_days = cal_module.monthrange(target.year, target.month) due_day_clamped = min(due_day, num_days) bill_date = date(target.year, target.month, due_day_clamped) if bill_date == target: due.append({**b, "due_date": bill_date}) return due def check_and_send_reminders(): sent = [] for days_ahead, label in [(3, "in 3 days"), (0, "TODAY")]: due = bills_due_in(days_ahead) if not due: continue total = sum(b.get("amount", 0) for b in due if b.get("amount")) # Mac notifications โ€” one per bill for b in due: amt = f"${b['amount']:.2f}" if b.get("amount") else "amount varies" mac_notification( f"Bill Due {label.title()}", f"{b['name']} โ€” {amt}" ) # Email โ€” one digest subject = f"๐ŸŒ™ Moon Budget: {len(due)} bill{'s' if len(due) != 1 else ''} due {label}" rows = "" for b in due: amt = f"${b['amount']:.2f}" if b.get("amount") else "โ€”" cat = b.get("category", "") rows += f""" {b['name']} {amt} {cat} """ total_row = f"${total:.2f}" if total else "" body_html = f"""

๐ŸŒ™ Moon Household Budget

Bill reminder โ€” due {label}

{rows} {f'' if total_row else ''}
Bill Amount Category
Total: {total_row}

Moon Household Budget ยท sent to bonna61@gmail.com

""" body_text = f"Moon Budget โ€” Bills due {label}:\n\n" for b in due: amt = f"${b['amount']:.2f}" if b.get("amount") else "varies" body_text += f" โ€ข {b['name']} โ€” {amt}\n" if total: body_text += f"\nTotal: ${total:.2f}\n" send_email(subject, body_html, body_text) sent.append(f"{len(due)} bills due {label}") return sent if __name__ == "__main__": results = check_and_send_reminders() if results: print("Sent reminders:", ", ".join(results)) else: print("No reminders to send today.")