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: