Building a Local SMS Sync Bridge: Extracting Messages from macOS without Twilio

Over the past development session, I tackled a specific infrastructure problem: reading SMS messages from a Samsung Android device and syncing them to a local macOS environment without external SMS gateway dependencies. This post walks through the technical decisions, architecture patterns, and implementation details.

The Challenge

The initial assumption was that SMS ingestion required Twilio credentials stored in .secrets/repos.env. However, after investigating the codebase, I discovered those credentials weren't available locally. The requirement changed: build a standalone sync mechanism that could pull SMS directly from the source device and integrate with existing message infrastructure.

What Was Done

  • Created /Users/cb/Documents/repos/tools/samsung_sms_sync.py — a Python script for local SMS extraction
  • Configured a macOS LaunchAgent at /Users/cb/Library/LaunchAgents/com.cb.samsung-sms-sync.plist for automated, periodic sync
  • Integrated with existing JADA SMS export infrastructure instead of rebuilding from scratch
  • Mapped Android SMS database schema to local conversation digests

Technical Architecture

Data Source: Android SMS Database

Android devices store SMS in a SQLite database accessible via ADB (Android Debug Bridge). The key files are:

  • /data/data/com.android.providers.telephony/databases/mmssms.db — main SMS/MMS storage
  • Tables: sms (individual messages), threads (conversation groupings), canonical_addresses (phone numbers)

Rather than accessing the device directly via KDE Connect or other bridge tools (which were checked but not ideal for automation), the approach leveraged ADB over USB:

brew install android-platform-tools
adb shell "sqlite3 /data/data/com.android.providers.telephony/databases/mmssms.db 'SELECT * FROM sms ORDER BY date DESC LIMIT 100;'"

This required installing Homebrew's android-platform-tools on Apple Silicon (M1/M2). The install path differs from Intel: /opt/homebrew/bin/adb rather than /usr/local/bin/adb. The Python script accounts for this via platform detection.

Local Message Infrastructure Integration

macOS includes a built-in SMS capability through the Messages app, which is scriptable via AppleScript and accessible to Python through the osascript bridge. However, the more reliable approach was direct SQLite export to a structured SMS digest format:

def extract_sms_thread(phone_number, db_path):
    """Query the Android SMS database for a specific contact."""
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    cursor.execute("""
        SELECT date, body, type 
        FROM sms 
        WHERE address = ? 
        ORDER BY date DESC
    """, (phone_number,))
    return cursor.fetchall()

The type field distinguishes between:

  • type=1 — received messages
  • type=2 — sent messages

Timestamps are in milliseconds since epoch; conversion to human-readable format is necessary for digest output.

LaunchAgent Configuration

The com.cb.samsung-sms-sync.plist file configures automated sync:

<?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.cb.samsung-sms-sync</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/python3</string>
    <string>/Users/cb/Documents/repos/tools/samsung_sms_sync.py</string>
  </array>
  <key>StartInterval</key>
  <integer>3600</integer>
  <key>StandardOutPath</key>
  <string>/var/log/samsung_sms_sync.log</string>
  <key>StandardErrorPath</key>
  <string>/var/log/samsung_sms_sync_error.log</string>
</dict>
</plist>

StartInterval: 3600 runs the sync every hour. This avoids constant polling while keeping messages relatively fresh for digest operations.

Key Architectural Decisions

Why Not Twilio?

Twilio is excellent for outbound SMS or API-driven workflows, but it requires:

  • Active subscription and API credentials
  • A phone number registered with Twilio
  • Network calls for every message lookup

For local read-only access to historical SMS, direct database extraction is cheaper and faster. The trade-off: requires USB debugging enabled on the Android device and ADB connectivity.

Why ADB Over Other Bridge Tools?

Several alternatives were evaluated:

  • KDE Connect: Designed for file sync and clipboard sharing; SMS access not primary use case
  • Apple Messages Service: Can send SMS via the Messages app, but doesn't directly expose Android message history
  • Mac Messages Database: ~/Library/Messages/chat.db exists but only contains messages routed through Messages.app, not synced Android conversations

ADB is the Android SDK's standard debugging protocol—it's battle-tested, scriptable, and provides direct database access without intermediate services.

Digest Generation Over Real-Time Sync

Rather than maintaining a bidirectional sync (which is complex and error-prone), the system generates digests on demand:

def generate_sms_digest(phone_numbers, date_range_days=7):
    """Compile recent SMS into a formatted email digest."""
    digests = {}
    for number in phone_numbers:
        messages = extract_sms_thread(number, DB_PATH)
        digests[number] = format_thread(messages, date_range_days)
    return digests

This approach:

  • Reduces complexity — no need to track sync state or handle conflicts