Skip to content

SNMiguel/deliveryradar

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DeliveryRadar

Gmail → Go → Discord
Automatic delivery tracking that just works.

Go SQLite Discord License


What is DeliveryRadar?

DeliveryRadar is a self-hosted daemon that monitors your Gmail inbox for shipping notification emails, extracts tracking numbers, queries carrier APIs for real-time status updates, and pushes rich formatted alerts to a Discord channel.

No web UI needed — Discord is your dashboard.

  Gmail Inbox          DeliveryRadar           Discord
 ┌───────────┐       ┌─────────────┐       ┌───────────┐
 │ 📧 Shipped│──────▶│ Parse       │──────▶│ 🚚 New!   │
 │ 📧 Tracked│       │ Track       │       │ 📍 Update │
 │ 📧 Order  │       │ Notify      │       │ ✅ Done!  │
 └───────────┘       └──────┬──────┘       │ 📊 Digest │
                            │              └───────────┘
                     ┌──────┴──────┐
                     │  SQLite DB  │
                     └─────────────┘

Features

Feature Description
Auto-Detection Polls Gmail every 5 minutes for shipping emails
Smart Parsing Extracts tracking numbers from email bodies, subjects, and HTML attributes
Multi-Carrier UPS, FedEx, USPS, DHL, Amazon (TBA), with generic fallback
Rich Embeds Beautiful Discord embeds for new packages, status changes, and deliveries
Nightly Digest Consolidated summary at 8 PM with all active and delivered packages
Zero Maintenance Runs as a background daemon with automatic restarts
Local-First SQLite database, no cloud services beyond Gmail API
Deduplication Tracks Gmail message IDs and digest dates to prevent duplicate alerts

Discord Notifications

New Package Detected

🚚 New Shipment Detected 📦 Description: iPhone 15 Pro Case 🏪 From: Amazon 📬 Carrier: UPS 🔢 Tracking: 1Z999AA10123456784 📍 Status: Pre-Transit 🗓️ ETA: Friday, Mar 27 🌍 Route: 🇨🇳 → 🇺🇸

Status Update

📍 Package Update 📦 iPhone 15 Pro Case (UPS) 🔄 Status: In Transit → Out for Delivery 📌 Location: Memphis, TN 🗓️ ETA: Today

Delivered

✅ Package Delivered! 📦 iPhone 15 Pro Case ⏱️ Transit Time: 3 days, 4 hours 📬 Carrier: UPS · 1Z999AA10123456784 🎉 Delivered at your door

Nightly Digest (8:00 PM)

📊 Delivery Digest — Wednesday, Mar 25

🚛 IN TRANSIT (2) • iPhone Case — UPS — ETA Tomorrow • Running Shoes — FedEx — ETA Fri Mar 27

🚪 OUT FOR DELIVERY (1) • Office Chair — Amazon — Expected Today

✅ DELIVERED TODAY (1) • HDMI Cable — USPS — Delivered 2:14 PM

📦 TOTAL ACTIVE: 3 packages


Demo

Terminal Output Discord Notification
Terminal output Discord notification

Quick Start

Prerequisites

1. Clone & Build

git clone https://github.com/SNMiguel/deliveryradar.git
cd deliveryradar

# Linux / macOS
CGO_ENABLED=1 go build -o deliveryradar ./cmd/deliveryradar

# Windows
set CGO_ENABLED=1 && go build -o deliveryradar.exe ./cmd/deliveryradar

2. Google Cloud Setup

  1. Go to Google Cloud Console
  2. Create a new project named deliveryradar
  3. Enable the Gmail API (APIs & Services → Library → search "Gmail API")
  4. Create OAuth 2.0 credentials:
    • Go to APIs & Services → Credentials → Create Credentials → OAuth client ID
    • Application type: Desktop app
    • Download the JSON and save as credentials.json in the project root
  5. Configure the OAuth consent screen:
    • Add your Gmail address as a test user
    • Scopes needed: gmail.readonly, gmail.labels

3. Discord Webhook

  1. Open your Discord server
  2. Go to the channel where you want notifications
  3. Channel Settings → Integrations → Webhooks → New Webhook
  4. Copy the webhook URL

4. Configure

cp internal/config/config.yaml.example config.yaml

Edit config.yaml:

gmail:
  credentials_file: "credentials.json"
  token_file: "token.json"
  poll_interval_minutes: 5

discord:
  webhook_url: "https://discord.com/api/webhooks/YOUR/WEBHOOK_URL"
  digest_time: "20:00"
  timezone: "America/Chicago"

database:
  path: "./deliveryradar.db"

tracking:
  ups_client_id: ""          # Optional: for native UPS API
  ups_client_secret: ""
  fedex_client_id: ""        # Optional: for native FedEx API
  fedex_client_secret: ""
  scrape_delay_seconds: 2

log:
  level: "info"              # debug | info | warn | error
  format: "text"             # text | json

5. First Run

./deliveryradar

On first run, DeliveryRadar will:

  1. Open a URL in your terminal for Gmail OAuth consent
  2. You paste the authorization code back
  3. Token is saved to token.json (never needs to be done again)
  4. Immediately polls your inbox and starts tracking
  5. Schedules the nightly digest

Project Structure

deliveryradar/
├── cmd/
│   └── deliveryradar/
│       └── main.go              # Entry point, daemon loop
├── internal/
│   ├── config/
│   │   ├── config.go            # Config struct and loader
│   │   ├── config.yaml.example  # Template for users
│   │   └── config_test.go       # Config validation tests
│   ├── models/
│   │   └── models.go            # Shared types (PackageStatus, TrackingResult)
│   ├── gmail/
│   │   ├── client.go            # OAuth2 setup, Gmail API client
│   │   ├── poller.go            # Poll inbox, filter shipping emails
│   │   ├── parser.go            # Extract tracking numbers from emails
│   │   └── parser_test.go       # Parser tests (12 test cases)
│   ├── tracking/
│   │   ├── engine.go            # Orchestrates carrier detection + status fetch
│   │   ├── carriers.go          # Carrier regex patterns
│   │   ├── carriers_test.go     # Carrier detection tests
│   │   ├── ups.go               # UPS REST API tracker
│   │   ├── fedex.go             # FedEx REST API tracker
│   │   ├── usps.go              # USPS tracker
│   │   ├── dhl.go               # DHL Tracking API tracker
│   │   └── generic.go           # AfterShip fallback scraper
│   ├── store/
│   │   ├── db.go                # SQLite init, migrations, types
│   │   ├── packages.go          # Package CRUD operations
│   │   ├── events.go            # Tracking event CRUD, digest log
│   │   └── store_test.go        # Database tests (7 test cases)
│   ├── discord/
│   │   ├── webhook.go           # Discord webhook sender with retry
│   │   ├── embeds.go            # Embed builders (new, update, delivered, digest)
│   │   └── flags.go             # Country code → flag emoji mapping
│   └── scheduler/
│       └── scheduler.go         # Cron job for nightly digest
├── migrations/
│   ├── 001_initial.sql          # Packages + events tables
│   └── 002_add_digest_log.sql   # Digest deduplication table
├── config.yaml                  # Your config (gitignored)
├── go.mod
├── go.sum
├── Makefile
└── README.md

Supported Carriers

Carrier Detection Pattern API Mode
UPS 1Z + 16 alphanumeric chars Native REST API (with credentials) or fallback
FedEx 12, 15, or 20 digits Native REST API (with credentials) or fallback
USPS 9[2-5] + 20-28 digits, or XX123456789US USPS Web Tools or fallback
DHL 10 digits or JD + 18 digits DHL Tracking API v2 or fallback
Amazon TBA + 12 digits AfterShip fallback
Other Context clues in email text AfterShip universal fallback

Getting Carrier API Keys (Optional)

Native API keys give higher rate limits and more reliable results, but are not required — the fallback scraper works for most cases.

UPS:

  1. Register at UPS Developer Kit
  2. Create an app → get Client ID and Client Secret
  3. Add to config.yaml under tracking.ups_client_id / tracking.ups_client_secret

FedEx:

  1. Register at FedEx Developer Portal
  2. Create a project → get API Key and Secret Key
  3. Add to config.yaml under tracking.fedex_client_id / tracking.fedex_client_secret

How It Works

Polling Cycle (every 5 minutes)

1. Query Gmail API for unread emails matching shipping keywords
2. For each email:
   a. Decode email body (base64, handle multipart/MIME)
   b. Extract tracking numbers using 3-tier strategy:
      ├── Structured HTML attributes (data-tracking-number, itemprop)
      ├── Known carrier regex patterns
      └── Context clues ("tracking number:", "track your order")
   c. Deduplicate against existing database entries
   d. Query carrier API for initial status
   e. Save to SQLite and send Discord notification
   f. Label email as processed in Gmail

Tracking Updates (every 30 minutes)

1. Query all non-delivered packages from database
2. For each package:
   a. Fetch latest status from carrier API
   b. Compare events against database to find new ones
   c. On status change → send Discord update embed
   d. On delivery → send celebration embed with transit time

Nightly Digest (configurable, default 8:00 PM)

1. Check digest_log table to prevent duplicates
2. Query packages grouped by status
3. Build consolidated Discord embed
4. Record digest as sent

Database

DeliveryRadar uses a local SQLite file (deliveryradar.db) with WAL mode for concurrent reads. The schema auto-migrates on startup.

Tables

Table Purpose
packages One row per tracked shipment (status, carrier, ETA, timestamps)
events Tracking history per package (timestamp, location, description)
digest_log Prevents duplicate nightly digest sends

Inspecting the Database

sqlite3 deliveryradar.db

-- View all packages
SELECT tracking_number, carrier, status, description FROM packages;

-- View recent events
SELECT p.tracking_number, e.description, e.location, e.timestamp
FROM events e JOIN packages p ON e.package_id = p.id
ORDER BY e.timestamp DESC LIMIT 20;

-- Check digest history
SELECT * FROM digest_log ORDER BY sent_at DESC;

Running as a Service

Windows (Task Scheduler)

  1. Open Task Scheduler → Create Basic Task
  2. Trigger: "When the computer starts"
  3. Action: Start a program
    • Program: C:\path\to\deliveryradar.exe
    • Start in: C:\path\to\ (directory with config.yaml)
  4. Check "Run whether user is logged on or not"

Linux (systemd)

# /etc/systemd/system/deliveryradar.service
[Unit]
Description=DeliveryRadar - Gmail Delivery Tracker
After=network.target

[Service]
Type=simple
User=youruser
WorkingDirectory=/opt/deliveryradar
ExecStart=/opt/deliveryradar/deliveryradar
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now deliveryradar
sudo journalctl -u deliveryradar -f  # View logs

macOS (launchd)

<!-- ~/Library/LaunchAgents/com.deliveryradar.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.deliveryradar</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/deliveryradar</string>
    </array>
    <key>WorkingDirectory</key>
    <string>/Users/youruser/deliveryradar</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/tmp/deliveryradar.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/deliveryradar.err</string>
</dict>
</plist>
launchctl load ~/Library/LaunchAgents/com.deliveryradar.plist

Configuration Reference

Key Type Default Description
gmail.credentials_file string credentials.json Path to Google OAuth credentials
gmail.token_file string token.json Path to stored OAuth token (auto-created)
gmail.poll_interval_minutes int 5 How often to check for new emails
gmail.label_filter string INBOX Gmail label to search within
discord.webhook_url string (required) Discord webhook URL for notifications
discord.digest_time string 20:00 When to send the nightly digest (HH:MM)
discord.timezone string America/Chicago Timezone for digest scheduling
database.path string ./deliveryradar.db Path to SQLite database file
tracking.ups_client_id string "" UPS API Client ID (optional)
tracking.ups_client_secret string "" UPS API Client Secret (optional)
tracking.fedex_client_id string "" FedEx API Key (optional)
tracking.fedex_client_secret string "" FedEx Secret Key (optional)
tracking.scrape_delay_seconds int 2 Politeness delay for fallback scraping
log.level string info Log verbosity: debug, info, warn, error
log.format string text Log format: text (dev) or json (prod)

Error Handling

Scenario Behavior
Gmail API rate limit (429) Exponential backoff, max 5 retries
Carrier API failure Log warning, retry next poll cycle, no Discord alert
Unrecognized tracking number Saved with carrier=unknown, skips tracking updates
SQLite locked Retry with 100ms backoff, up to 10 attempts
Discord webhook failure Log error, retry once after 5 seconds
Missing config field Fatal error on startup with clear message
OAuth token expired Auto-refresh via golang.org/x/oauth2

Development

Running Tests

# All tests
go test ./... -v

# Specific package
go test ./internal/gmail/... -v
go test ./internal/store/... -v

# With race detection
go test ./... -v -race

Test Coverage

Package Tests Coverage
internal/config 7 tests Config loading, defaults, validation
internal/gmail 19 tests Tracking number extraction across carriers
internal/store 7 tests CRUD operations, digest log, in-memory SQLite
internal/tracking 12 tests Carrier detection regex patterns
Total 45 tests

Build Commands

# Build
make build

# Run directly
make run

# Run tests
make test

# Lint (requires golangci-lint)
make lint

# Clean artifacts
make clean

Security

  • credentials.json, token.json, and config.yaml are in .gitignorenever commit them
  • The Discord webhook URL is treated as a secret and is never logged
  • Gmail OAuth scope is gmail.readonly + gmail.labels — no write access to email content
  • The service has no open ports — it is outbound-only
  • SQLite database is local and not exposed over any network

Troubleshooting

"credentials.json not found"

Download your OAuth credentials from Google Cloud Console and place them in the project root directory.

"token expired" or authentication errors

Delete token.json and restart DeliveryRadar. It will re-prompt for OAuth authorization.

No emails being detected

  • Check that emails match the search query: is:unread subject:(shipped OR tracking OR delivery OR order OR shipment)
  • Verify the deliveryradar-processed label isn't already applied to emails
  • Set log.level: "debug" to see detailed polling output

Discord notifications not appearing

  • Verify the webhook URL in config.yaml is correct
  • Test the webhook manually: curl -X POST -H "Content-Type: application/json" -d '{"content":"test"}' YOUR_WEBHOOK_URL
  • Check logs for Discord API errors

Build fails with CGo errors

Ensure you have GCC installed and CGO_ENABLED=1 is set. On Windows, use TDM-GCC.


License

MIT License. See LICENSE for details.


Built with Go, SQLite, and too many tracking numbers.

About

Self-hosted delivery tracker built in Go — monitors Gmail via Google OAuth2, extracts tracking numbers from emails, queries UPS/FedEx/USPS/DHL APIs, and pushes real-time shipping updates to Discord. Backed by SQLite.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors