Compare commits

..

1 Commits

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

@ -392,7 +392,7 @@ curl 'https://future.ourclublogin.com/api/Scheduling/CancelAppointment' \
-H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Site: same-origin' \ -H 'Sec-Fetch-Site: same-origin' \
-H 'Priority: u=0' \ -H 'Priority: u=0' \
--data-raw '{"ClubId":2,"LoggedInCustomerId":21873,"PrimaryCustomerId":6964,"AdditionalCustomerIds":[],"AppointmentItemId":323,"SelectedBooks":[{"Id":2,"Name":"Pickleball Court - 2","ResourceTypeId":2,"AssignedResourceId":130,"IsAssignedResourceSelectable":true}],"PackageItemId":0,"PackageQuantity":0,"ChangeFeeId":0,"StartDate":"2026-01-19T17:30:00-08:00","UserDisplayedPayNowGrandTotal":0,"DisplayedAmountDueAtTimeOfService":0,"CancellationAppointmentId":0}' --data-raw '{"ClubId":2,"LoggedInCustomerId":21873,"PrimaryCustomerId":6964,"AdditionalCustomerIds":[],"AppointmentItemId":323,"SelectedBooks":[{"Id":2,"Name":"Pickleball Court - 2","ResourceTypeId":2,"AssignedResourceId":130,"IsAssignedResourceSelectable":true}],"PackageItemId":0,"PackageQuantity":0,"ChangeFeeId":0,"StartDate":"2026-01-15T17:30:00-08:00","UserDisplayedPayNowGrandTotal":0,"DisplayedAmountDueAtTimeOfService":0,"CancellationAppointmentId":0}'
curl 'https://future.ourclublogin.com/api/TransactionProcessing/BookAppointmentOnAccount' \ curl 'https://future.ourclublogin.com/api/TransactionProcessing/BookAppointmentOnAccount' \
@ -416,7 +416,7 @@ curl 'https://future.ourclublogin.com/api/Scheduling/CancelAppointment' \
-H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Site: same-origin' \ -H 'Sec-Fetch-Site: same-origin' \
-H 'Priority: u=0' \ -H 'Priority: u=0' \
--data-raw '{"ClubId":2,"LoggedInCustomerId":21873,"PrimaryCustomerId":16250,"AdditionalCustomerIds":[],"AppointmentItemId":323,"SelectedBooks":[{"Id":3,"Name":"Pickleball Court - 3","ResourceTypeId":2,"AssignedResourceId":130,"IsAssignedResourceSelectable":true}],"PackageItemId":0,"PackageQuantity":0,"ChangeFeeId":0,"StartDate":"2026-01-19T17:30:00-08:00","UserDisplayedPayNowGrandTotal":0,"DisplayedAmountDueAtTimeOfService":0,"CancellationAppointmentId":0}' --data-raw '{"ClubId":2,"LoggedInCustomerId":21873,"PrimaryCustomerId":16250,"AdditionalCustomerIds":[],"AppointmentItemId":323,"SelectedBooks":[{"Id":3,"Name":"Pickleball Court - 3","ResourceTypeId":2,"AssignedResourceId":130,"IsAssignedResourceSelectable":true}],"PackageItemId":0,"PackageQuantity":0,"ChangeFeeId":0,"StartDate":"2026-01-15T17:30:00-08:00","UserDisplayedPayNowGrandTotal":0,"DisplayedAmountDueAtTimeOfService":0,"CancellationAppointmentId":0}'
curl 'https://future.ourclublogin.com/api/TransactionProcessing/BookAppointmentOnAccount' \ curl 'https://future.ourclublogin.com/api/TransactionProcessing/BookAppointmentOnAccount' \
@ -440,7 +440,7 @@ curl 'https://future.ourclublogin.com/api/Scheduling/CancelAppointment' \
-H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Site: same-origin' \ -H 'Sec-Fetch-Site: same-origin' \
-H 'Priority: u=0' \ -H 'Priority: u=0' \
--data-raw '{"ClubId":2,"LoggedInCustomerId":21873,"PrimaryCustomerId":14563,"AdditionalCustomerIds":[],"AppointmentItemId":323,"SelectedBooks":[{"Id":139,"Name":"Pickleball Court - 4","ResourceTypeId":2,"AssignedResourceId":130,"IsAssignedResourceSelectable":true}],"PackageItemId":0,"PackageQuantity":0,"ChangeFeeId":0,"StartDate":"2026-01-19T17:30:00-08:00","UserDisplayedPayNowGrandTotal":0,"DisplayedAmountDueAtTimeOfService":0,"CancellationAppointmentId":0}' --data-raw '{"ClubId":2,"LoggedInCustomerId":21873,"PrimaryCustomerId":14563,"AdditionalCustomerIds":[],"AppointmentItemId":323,"SelectedBooks":[{"Id":139,"Name":"Pickleball Court - 4","ResourceTypeId":2,"AssignedResourceId":130,"IsAssignedResourceSelectable":true}],"PackageItemId":0,"PackageQuantity":0,"ChangeFeeId":0,"StartDate":"2026-01-15T17:30:00-08:00","UserDisplayedPayNowGrandTotal":0,"DisplayedAmountDueAtTimeOfService":0,"CancellationAppointmentId":0}'
# do this court 1 last!!! # do this court 1 last!!!
curl 'https://future.ourclublogin.com/api/TransactionProcessing/BookAppointmentOnAccount' \ curl 'https://future.ourclublogin.com/api/TransactionProcessing/BookAppointmentOnAccount' \
@ -464,7 +464,7 @@ curl 'https://future.ourclublogin.com/api/Scheduling/CancelAppointment' \
-H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Site: same-origin' \ -H 'Sec-Fetch-Site: same-origin' \
-H 'Priority: u=0' \ -H 'Priority: u=0' \
--data-raw '{"ClubId":2,"LoggedInCustomerId":21873,"PrimaryCustomerId":21873,"AdditionalCustomerIds":[],"AppointmentItemId":323,"SelectedBooks":[{"Id":1,"Name":"Pickleball Court - 1","ResourceTypeId":2,"AssignedResourceId":130,"IsAssignedResourceSelectable":true}],"PackageItemId":0,"PackageQuantity":0,"ChangeFeeId":0,"StartDate":"2026-01-19T17:30:00-08:00","UserDisplayedPayNowGrandTotal":0,"DisplayedAmountDueAtTimeOfService":0,"CancellationAppointmentId":0}' --data-raw '{"ClubId":2,"LoggedInCustomerId":21873,"PrimaryCustomerId":21873,"AdditionalCustomerIds":[],"AppointmentItemId":323,"SelectedBooks":[{"Id":1,"Name":"Pickleball Court - 1","ResourceTypeId":2,"AssignedResourceId":130,"IsAssignedResourceSelectable":true}],"PackageItemId":0,"PackageQuantity":0,"ChangeFeeId":0,"StartDate":"2026-01-15T17:30:00-08:00","UserDisplayedPayNowGrandTotal":0,"DisplayedAmountDueAtTimeOfService":0,"CancellationAppointmentId":0}'
# When it fails to book!!!!! # When it fails to book!!!!!

@ -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

@ -1,5 +0,0 @@
alias HARBORBOT_RESTART="sudo systemctl daemon-reload;sudo systemctl restart harbor-bot"
alias HARBORBOT_LOGS="sudo journalctl -u harbor-bot -f"
alias HARBORBOT_STOP="sudo systemctl stop harbor-bot"
alias HARBORBOT_START="sudo systemctl start harbor-bot"
alias HARBORBOT_STATUS="sudo systemctl status harbor-bot"

@ -45,7 +45,8 @@
{"Time": "04:29:00-08:00", "Attempts": 2, "DelaySeconds": 10}, {"Time": "04:29:00-08:00", "Attempts": 2, "DelaySeconds": 10},
{"Time": "04:31:00-08:00", "Attempts": 2, "DelaySeconds": 10}, {"Time": "04:31:00-08:00", "Attempts": 2, "DelaySeconds": 10},
{"Time": "04:35:00-08:00", "Attempts": 1, "DelaySeconds": 0}, {"Time": "04:35:00-08:00", "Attempts": 1, "DelaySeconds": 0},
{"Time": "21:37:00-08:00", "Attempts": 4, "DelaySeconds": 10} {"Time": "20:05:00-08:00", "Attempts": 4, "DelaySeconds": 10},
{"Time": "20:11:00-08:00", "Attempts": 4, "DelaySeconds": 10}
] ]
}, },
{ {
@ -56,8 +57,8 @@
{"Time": "06:30:00-08:00", "Attempts": 2, "DelaySeconds": 10}, {"Time": "06:30:00-08:00", "Attempts": 2, "DelaySeconds": 10},
{"Time": "06:55:00-08:00", "Attempts": 2, "DelaySeconds": 10}, {"Time": "06:55:00-08:00", "Attempts": 2, "DelaySeconds": 10},
{"Time": "07:00:00-08:00", "Attempts": 1, "DelaySeconds": 0}, {"Time": "07:00:00-08:00", "Attempts": 1, "DelaySeconds": 0},
{"Time": "07:01:00-08:00", "Attempts": 2, "DelaySeconds": 10}, {"Time": "07:01:00-08:00", "Attempts": 2, "DelaySeconds": 10},
{"Time": "07:02:00-08:00", "Attempts": 2, "DelaySeconds": 10} {"Time": "07:02:00-08:00", "Attempts": 2, "DelaySeconds": 10}
] ]
} }
], ],
@ -117,6 +118,14 @@
{"Court": "Court_4", "Account": "Treve"} {"Court": "Court_4", "Account": "Treve"}
] ]
}, },
{
"Day": "Sunday",
"Time": "15:30:00-08:00",
"ReservationBehavior": "Test_Reservation_Type",
"Courts": [
{"Court": "Court_3", "Account": "Saleh"}
]
},
{ {
"Day": "Saturday", "Day": "Saturday",
"Time": "14:00:00-08:00", "Time": "14:00:00-08:00",

@ -1,17 +0,0 @@
[Unit]
Description=Harbor Bot Pickleball Reservation Service -- put in /etc/systemd/system/harbor-bot.service
After=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/harbor-bot
ExecStart=/usr/bin/python3 -u /home/pi/harbor-bot/harbor_bot.py --daemon --live
Restart=always
RestartSec=30
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target

@ -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')}")
@ -724,20 +866,9 @@ class HarborBot:
self._print_upcoming_reservations() self._print_upcoming_reservations()
last_status_print = datetime.now() last_status_print = datetime.now()
last_date = datetime.now().date()
while True: while True:
try: try:
# Check for day change
current_date = datetime.now().date()
if current_date != last_date:
print(f"\n[INFO] {'='*50}")
print(f"[INFO] New day detected: {current_date.strftime('%A, %B %d, %Y')}")
print(f"[INFO] {'='*50}")
self._print_upcoming_reservations()
last_date = current_date
last_status_print = datetime.now()
self._daemon_iteration(dry_run) self._daemon_iteration(dry_run)
# Print status every 30 minutes # Print status every 30 minutes
@ -757,7 +888,6 @@ class HarborBot:
traceback.print_exc() traceback.print_exc()
print("[INFO] Sleeping 60s before retry...") print("[INFO] Sleeping 60s before retry...")
time.sleep(60) time.sleep(60)
time.sleep(60)
def _print_upcoming_reservations(self): def _print_upcoming_reservations(self):
"""Print status of upcoming reservations.""" """Print status of upcoming reservations."""
@ -883,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')}")
@ -976,6 +1107,15 @@ class HarborBot:
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):
""" """
Test booking a specific court for a specific account. Test booking a specific court for a specific account.
@ -1017,11 +1157,133 @@ 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():
"""Main entry point.""" """Main entry point."""
@ -1030,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)")
@ -1056,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
@ -1069,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:

@ -69,7 +69,7 @@ curl 'https://future.ourclublogin.com/api/CustomerAuth/CustomerLogin' \
* Add startup configuration validation to ensure that the AttemptSchedule is configured properly, with scheduled times incrementing as required by the algorithm. * Add startup configuration validation to ensure that the AttemptSchedule is configured properly, with scheduled times incrementing as required by the algorithm.
## ##
* This will run on a raspberry pi on linux, implement a pure python method for sending an email everytime a court is successfully booked. Be sure to include what court was booked, what time it was booked for, and the user name and ID that the booking was creating under. config.json will obviously need to be updated to support any number of emails that are expected to receive these success emails. * This will run on a raspberry pi on linux, implement a pure python method for sending an email everytime a court is successfully booked. Be sure to include what court was booked, what time it was booked for, and the user name and ID that the booking was creating under.
@ -82,5 +82,3 @@ python harbor_bot.py --test-book Court_1 Saleh 2026-01-19 "19:00:00-08:00"
python harbor_bot.py --test-book Court_1 Saleh 2026-01-19 "19:00:00-08:00" --live python harbor_bot.py --test-book Court_1 Saleh 2026-01-19 "19:00:00-08:00" --live
python harbor_bot.py --daemon --live python harbor_bot.py --daemon --live
# python harbor_bot.py --test-email (See smtp-email-notifications-attempt branch, gmail smtp doesn't actually work)
python harbor_bot.py --show-config
Loading…
Cancel
Save