From 9bb6595f43233dd14978c4b2982e95da4cd07ece Mon Sep 17 00:00:00 2001 From: Ibraheem Saleh Date: Sun, 11 Jan 2026 21:06:37 -0800 Subject: [PATCH] email-attempt-failed --- README.md | 31 ++++++ harbor_bot.py | 282 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 311 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 90c4faf..5ea03b8 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,37 @@ Scheduled reservation configurations: } ``` +### Email Notifications (Optional) +Configure email notifications to receive alerts when courts are successfully booked: + +```json +{ + "Email": { + "Enabled": true, + "SmtpServer": "smtp.gmail.com", + "SmtpPort": 587, + "SmtpUsername": "your-email@gmail.com", + "SmtpPassword": "your-app-password", + "FromAddress": "your-email@gmail.com", + "ToAddresses": [ + "recipient1@example.com", + "recipient2@example.com" + ], + "UseTLS": true + } +} +``` + +**Gmail Setup:** +1. Enable 2-factor authentication on your Google account +2. Generate an App Password: Google Account → Security → App Passwords +3. Use the 16-character app password as `SmtpPassword` + +**Other SMTP Providers:** +- Outlook: `smtp.office365.com`, port 587 +- Yahoo: `smtp.mail.yahoo.com`, port 587 +- Custom SMTP: Use your provider's settings + ## Files - `config.json` - Main configuration diff --git a/harbor_bot.py b/harbor_bot.py index 142a704..7728dfc 100644 --- a/harbor_bot.py +++ b/harbor_bot.py @@ -10,6 +10,9 @@ import os import sys import time import re +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart from datetime import datetime, timedelta, timezone from typing import Dict, List, Optional, Any from dataclasses import dataclass, field @@ -84,6 +87,18 @@ class Reservation: courts: List[CourtReservation] +@dataclass +class EmailConfig: + enabled: bool + smtp_server: str + smtp_port: int + smtp_username: str + smtp_password: str + from_address: str + to_addresses: List[str] + use_tls: bool = True + + @dataclass class Config: base_api_url: str @@ -97,6 +112,7 @@ class Config: courts: Dict[str, Court] reservation_behaviors: Dict[str, ReservationBehavior] reservations: List[Reservation] + email_config: Optional[EmailConfig] = None @dataclass @@ -257,6 +273,30 @@ def load_config(config_path: str = "config.json") -> Config: courts=court_reservations )) + # Parse email configuration (optional) + email_config = None + if "Email" in data: + email_data = data["Email"] + if email_data.get("Enabled", False): + required_email_fields = ["SmtpServer", "SmtpPort", "SmtpUsername", "SmtpPassword", "FromAddress", "ToAddresses"] + for field_name in required_email_fields: + if field_name not in email_data: + raise ValueError(f"Email is enabled but missing required field: {field_name}") + + if not isinstance(email_data["ToAddresses"], list) or len(email_data["ToAddresses"]) == 0: + raise ValueError("Email ToAddresses must be a non-empty list of email addresses") + + email_config = EmailConfig( + enabled=True, + smtp_server=email_data["SmtpServer"], + smtp_port=email_data["SmtpPort"], + smtp_username=email_data["SmtpUsername"], + smtp_password=email_data["SmtpPassword"], + from_address=email_data["FromAddress"], + to_addresses=email_data["ToAddresses"], + use_tls=email_data.get("UseTLS", True) + ) + return Config( base_api_url=data["BaseAPIUrl"], company_id=data["CompanyId"], @@ -268,7 +308,8 @@ def load_config(config_path: str = "config.json") -> Config: accounts=accounts, courts=courts, reservation_behaviors=behaviors, - reservations=reservations + reservations=reservations, + email_config=email_config ) @@ -345,6 +386,106 @@ class HarborBot: with open(self.success_file, 'w') as f: json.dump(self.success_tracking, f, indent=2) + def _send_success_email(self, court: Court, account: Account, reservation_datetime: str): + """ + Send email notification for successful booking. + + Args: + court: The court that was booked + account: The account the booking was made under + reservation_datetime: The datetime of the reservation + """ + if not self.config.email_config or not self.config.email_config.enabled: + return + + email_cfg = self.config.email_config + + # Parse the reservation datetime for display + try: + # Format: "2026-01-19T17:30:00-08:00" + dt_part = reservation_datetime[:19] + tz_part = reservation_datetime[19:] + dt = datetime.strptime(dt_part, "%Y-%m-%dT%H:%M:%S") + formatted_date = dt.strftime("%A, %B %d, %Y") + formatted_time = dt.strftime("%I:%M %p") + except: + formatted_date = reservation_datetime + formatted_time = "" + + subject = f"✅ Court Booked: {court.name} on {formatted_date}" + + body_text = f"""Harbor Bot - Successful Court Reservation + +Court: {court.name} +Date: {formatted_date} +Time: {formatted_time} (Pacific) + +Booked Under: + Name: {account.name} + Account ID: {account.id} + +Reservation Details: + Full DateTime: {reservation_datetime} + Court ID: {court.id} + +--- +This is an automated message from Harbor Bot. +""" + + body_html = f""" + + +
+

✅ Court Booked!

+
+
+

{court.name}

+ + + + + + + + + + + + + +
Date:{formatted_date}
Time:{formatted_time} (Pacific)
Booked Under:{account.name} (ID: {account.id})
+
+
+ This is an automated message from Harbor Bot. +
+ + +""" + + try: + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = email_cfg.from_address + msg["To"] = ", ".join(email_cfg.to_addresses) + + msg.attach(MIMEText(body_text, "plain")) + msg.attach(MIMEText(body_html, "html")) + + if email_cfg.use_tls: + server = smtplib.SMTP(email_cfg.smtp_server, email_cfg.smtp_port) + server.starttls() + else: + server = smtplib.SMTP_SSL(email_cfg.smtp_server, email_cfg.smtp_port) + + server.login(email_cfg.smtp_username, email_cfg.smtp_password) + server.sendmail(email_cfg.from_address, email_cfg.to_addresses, msg.as_string()) + server.quit() + + print(f" [EMAIL] Notification sent to {len(email_cfg.to_addresses)} recipient(s)") + + except Exception as e: + print(f" [EMAIL ERROR] Failed to send notification: {e}") + def _get_reservation_key(self, reservation_date: str, court: str, account: str) -> str: """Generate unique key for tracking reservation success.""" return f"{reservation_date}_{court}_{account}" @@ -690,6 +831,7 @@ class HarborBot: if result.get("Success"): print(f" [SUCCESS] Booked {court.name} for {account.name}") self._mark_reservation_successful(reservation_date_str, court_res.court, court_res.account) + self._send_success_email(court, account, reservation_datetime) break else: print(f" [FAILED] {result.get('Message', 'Unknown error')}") @@ -871,6 +1013,7 @@ class HarborBot: if result.get("Success"): print(f" [SUCCESS] Booked {court.name} for {account.name}") self._mark_reservation_successful(reservation_date_str, court_res.court, court_res.account) + self._send_success_email(court, account, reservation_datetime) break else: print(f" [FAILED] {result.get('Message', 'Unknown error')}") @@ -963,6 +1106,15 @@ class HarborBot: print(f" - {res.day} @ {res.time} ({res.reservation_behavior}){trigger_info}") for cr in res.courts: print(f" {cr.court} -> {cr.account}") + + print(f"\nEmail Notifications:") + if self.config.email_config and self.config.email_config.enabled: + print(f" Status: ENABLED") + print(f" SMTP Server: {self.config.email_config.smtp_server}:{self.config.email_config.smtp_port}") + print(f" From: {self.config.email_config.from_address}") + print(f" To: {', '.join(self.config.email_config.to_addresses)}") + else: + print(f" Status: DISABLED") def test_booking(self, court_name: str, account_name: str, date_str: str, time_str: str, dry_run: bool = True): """ @@ -1005,10 +1157,132 @@ class HarborBot: if result.get("Success"): print(f"\n[SUCCESS] Booked {court.name} for {account.name}") + self._send_success_email(court, account, reservation_datetime) else: print(f"\n[FAILED] {result.get('Message', 'Unknown error')}") print(f"[DEBUG] Full response: {json.dumps(result, indent=2)}") + + def test_email(self): + """Send a test email to verify email configuration.""" + if not self.config.email_config: + print("[ERROR] No email configuration found in config.json") + print("[INFO] Add an 'Email' section to your config.json file") + return False + + if not self.config.email_config.enabled: + print("[ERROR] Email is disabled in configuration") + print("[INFO] Set 'Enabled': true in the Email section of config.json") + return False + + email_cfg = self.config.email_config + + print(f"\n[TEST EMAIL] Sending test email...") + print(f" SMTP Server: {email_cfg.smtp_server}:{email_cfg.smtp_port}") + print(f" From: {email_cfg.from_address}") + print(f" To: {', '.join(email_cfg.to_addresses)}") + print(f" TLS: {email_cfg.use_tls}") + + # Create test email content + subject = "🏓 Harbor Bot - Test Email" + + body_text = f"""Harbor Bot - Email Test + +This is a test email from Harbor Bot to verify your email configuration is working correctly. + +Configuration: + SMTP Server: {email_cfg.smtp_server}:{email_cfg.smtp_port} + From: {email_cfg.from_address} + TLS Enabled: {email_cfg.use_tls} + +If you received this email, your configuration is correct! + +--- +Sent at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +""" + + body_html = f""" + + +
+

🏓 Harbor Bot

+

Email Test

+
+
+

This is a test email from Harbor Bot to verify your email configuration is working correctly.

+ +

Configuration

+ + + + + + + + + + + + + +
SMTP Server:{email_cfg.smtp_server}:{email_cfg.smtp_port}
From:{email_cfg.from_address}
TLS Enabled:{email_cfg.use_tls}
+ +

+ ✅ If you received this email, your configuration is correct! +

+
+
+ Sent at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +
+ + +""" + + try: + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = email_cfg.from_address + msg["To"] = ", ".join(email_cfg.to_addresses) + + msg.attach(MIMEText(body_text, "plain")) + msg.attach(MIMEText(body_html, "html")) + + print(f"\n[INFO] Connecting to SMTP server...") + + if email_cfg.use_tls: + server = smtplib.SMTP(email_cfg.smtp_server, email_cfg.smtp_port) + server.set_debuglevel(1) # Enable debug output + print(f"[INFO] Starting TLS...") + server.starttls() + else: + server = smtplib.SMTP_SSL(email_cfg.smtp_server, email_cfg.smtp_port) + server.set_debuglevel(1) + + print(f"[INFO] Logging in as {email_cfg.smtp_username}...") + server.login(email_cfg.smtp_username, email_cfg.smtp_password) + + print(f"[INFO] Sending email...") + server.sendmail(email_cfg.from_address, email_cfg.to_addresses, msg.as_string()) + server.quit() + + print(f"\n[SUCCESS] Test email sent successfully!") + print(f"[INFO] Check inbox of: {', '.join(email_cfg.to_addresses)}") + return True + + except smtplib.SMTPAuthenticationError as e: + print(f"\n[ERROR] Authentication failed: {e}") + print(f"\n[HELP] For Gmail:") + print(f" 1. Enable 2-factor authentication on your Google account") + print(f" 2. Go to: https://myaccount.google.com/apppasswords") + print(f" 3. Generate an App Password for 'Mail'") + print(f" 4. Use the 16-character app password (no spaces) as SmtpPassword") + return False + except smtplib.SMTPException as e: + print(f"\n[ERROR] SMTP error: {e}") + return False + except Exception as e: + print(f"\n[ERROR] Failed to send test email: {e}") + return False def main(): @@ -1018,6 +1292,7 @@ def main(): parser = argparse.ArgumentParser(description="Harbor Isles Pickleball Court Reservation Bot") parser.add_argument("--config", default="config.json", help="Path to configuration file") parser.add_argument("--test-login", action="store_true", help="Test login functionality") + parser.add_argument("--test-email", action="store_true", help="Send a test email to verify email configuration") parser.add_argument("--show-config", action="store_true", help="Display loaded configuration") parser.add_argument("--run", action="store_true", help="Run once and check for reservations (one-shot mode)") parser.add_argument("--daemon", action="store_true", help="Run continuously as a daemon (recommended)") @@ -1044,6 +1319,9 @@ def main(): if args.test_login: bot.test_login() + if args.test_email: + bot.test_email() + if args.test_book: court, account, date, time_val = args.test_book dry_run = not args.live @@ -1057,7 +1335,7 @@ def main(): dry_run = not args.live bot.run_daemon(dry_run=dry_run) - if not any([args.show_config, args.test_login, args.run, args.test_book, args.daemon]): + if not any([args.show_config, args.test_login, args.test_email, args.run, args.test_book, args.daemon]): parser.print_help() except FileNotFoundError as e: