Compare commits

...

1 Commits

Author SHA1 Message Date
Ibraheem Saleh 9bb6595f43 email-attempt-failed 8 months ago

@ -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 ## Files
- `config.json` - Main configuration - `config.json` - Main configuration

@ -10,6 +10,9 @@ import os
import sys import sys
import time import time
import re import re
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional, Any from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -84,6 +87,18 @@ class Reservation:
courts: List[CourtReservation] 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 @dataclass
class Config: class Config:
base_api_url: str base_api_url: str
@ -97,6 +112,7 @@ class Config:
courts: Dict[str, Court] courts: Dict[str, Court]
reservation_behaviors: Dict[str, ReservationBehavior] reservation_behaviors: Dict[str, ReservationBehavior]
reservations: List[Reservation] reservations: List[Reservation]
email_config: Optional[EmailConfig] = None
@dataclass @dataclass
@ -257,6 +273,30 @@ def load_config(config_path: str = "config.json") -> Config:
courts=court_reservations 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( return Config(
base_api_url=data["BaseAPIUrl"], base_api_url=data["BaseAPIUrl"],
company_id=data["CompanyId"], company_id=data["CompanyId"],
@ -268,7 +308,8 @@ def load_config(config_path: str = "config.json") -> Config:
accounts=accounts, accounts=accounts,
courts=courts, courts=courts,
reservation_behaviors=behaviors, 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: with open(self.success_file, 'w') as f:
json.dump(self.success_tracking, f, indent=2) 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"""
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #4CAF50; color: white; padding: 20px; text-align: center;">
<h1 style="margin: 0;"> Court Booked!</h1>
</div>
<div style="padding: 20px; background-color: #f9f9f9;">
<h2 style="color: #333; margin-top: 0;">{court.name}</h2>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Date:</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">{formatted_date}</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Time:</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">{formatted_time} (Pacific)</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Booked Under:</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">{account.name} (ID: {account.id})</td>
</tr>
</table>
</div>
<div style="padding: 10px; text-align: center; color: #666; font-size: 12px;">
This is an automated message from Harbor Bot.
</div>
</body>
</html>
"""
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: def _get_reservation_key(self, reservation_date: str, court: str, account: str) -> str:
"""Generate unique key for tracking reservation success.""" """Generate unique key for tracking reservation success."""
return f"{reservation_date}_{court}_{account}" return f"{reservation_date}_{court}_{account}"
@ -690,6 +831,7 @@ class HarborBot:
if result.get("Success"): if result.get("Success"):
print(f" [SUCCESS] Booked {court.name} for {account.name}") print(f" [SUCCESS] Booked {court.name} for {account.name}")
self._mark_reservation_successful(reservation_date_str, court_res.court, court_res.account) self._mark_reservation_successful(reservation_date_str, court_res.court, court_res.account)
self._send_success_email(court, account, reservation_datetime)
break break
else: else:
print(f" [FAILED] {result.get('Message', 'Unknown error')}") print(f" [FAILED] {result.get('Message', 'Unknown error')}")
@ -871,6 +1013,7 @@ class HarborBot:
if result.get("Success"): if result.get("Success"):
print(f" [SUCCESS] Booked {court.name} for {account.name}") print(f" [SUCCESS] Booked {court.name} for {account.name}")
self._mark_reservation_successful(reservation_date_str, court_res.court, court_res.account) self._mark_reservation_successful(reservation_date_str, court_res.court, court_res.account)
self._send_success_email(court, account, reservation_datetime)
break break
else: else:
print(f" [FAILED] {result.get('Message', 'Unknown error')}") 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}") print(f" - {res.day} @ {res.time} ({res.reservation_behavior}){trigger_info}")
for cr in res.courts: for cr in res.courts:
print(f" {cr.court} -> {cr.account}") 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): 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"): if result.get("Success"):
print(f"\n[SUCCESS] Booked {court.name} for {account.name}") print(f"\n[SUCCESS] Booked {court.name} for {account.name}")
self._send_success_email(court, account, reservation_datetime)
else: else:
print(f"\n[FAILED] {result.get('Message', 'Unknown error')}") print(f"\n[FAILED] {result.get('Message', 'Unknown error')}")
print(f"[DEBUG] Full response: {json.dumps(result, indent=2)}") 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"""
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background-color: #2196F3; color: white; padding: 20px; text-align: center;">
<h1 style="margin: 0;">🏓 Harbor Bot</h1>
<p style="margin: 10px 0 0 0;">Email Test</p>
</div>
<div style="padding: 20px; background-color: #f9f9f9;">
<p>This is a test email from Harbor Bot to verify your email configuration is working correctly.</p>
<h3 style="color: #333;">Configuration</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>SMTP Server:</strong></td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">{email_cfg.smtp_server}:{email_cfg.smtp_port}</td>
</tr>
<tr>
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>From:</strong></td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">{email_cfg.from_address}</td>
</tr>
<tr>
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>TLS Enabled:</strong></td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">{email_cfg.use_tls}</td>
</tr>
</table>
<p style="margin-top: 20px; padding: 15px; background-color: #e8f5e9; border-radius: 5px; color: #2e7d32;">
If you received this email, your configuration is correct!
</p>
</div>
<div style="padding: 10px; text-align: center; color: #666; font-size: 12px;">
Sent at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
</div>
</body>
</html>
"""
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(): def main():
@ -1018,6 +1292,7 @@ def main():
parser = argparse.ArgumentParser(description="Harbor Isles Pickleball Court Reservation Bot") 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("--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-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("--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("--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)") parser.add_argument("--daemon", action="store_true", help="Run continuously as a daemon (recommended)")
@ -1044,6 +1319,9 @@ def main():
if args.test_login: if args.test_login:
bot.test_login() bot.test_login()
if args.test_email:
bot.test_email()
if args.test_book: if args.test_book:
court, account, date, time_val = args.test_book court, account, date, time_val = args.test_book
dry_run = not args.live dry_run = not args.live
@ -1057,7 +1335,7 @@ def main():
dry_run = not args.live dry_run = not args.live
bot.run_daemon(dry_run=dry_run) 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() parser.print_help()
except FileNotFoundError as e: except FileNotFoundError as e:

Loading…
Cancel
Save