Skip to content

peter2380123/sticky-notes-sync-win2ios

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

26 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Sticky Notes to iOS Sync

One-way sync tool for Windows Sticky Notes to iOS Notes app via Microsoft Graph API and Gmail IMAP.

Microsoft Cloud (Sticky Notes)
        |  read via Graph API
   sync_notes.py
        |  push via IMAP
   Gmail "Notes" folder
        |  auto-sync
   iOS Notes app

Additions (modifications, deletions) in Sticky Notes are reflected in iOS Notes on the next sync.

Prerequisites

  • Python 3.10+
  • A Microsoft account with Sticky Notes
  • A dedicated Gmail account used only for this sync (see Security β€” strongly recommended, not your primary Gmail)
  • That dedicated Gmail account configured in iOS Settings > Mail with the Notes toggle enabled

Setup

1. Clone and install

git clone https://github.com/peter2380123/sticky-notes-sync-win2ios.git
cd sync_ios_win_sticky_notes
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements-lock.txt   # reproducible, fully pinned
# or: pip install -r requirements.txt   # top-level deps only

After cloning, install the pre-commit hook that blocks accidental commits of secret files:

cp scripts/pre-commit-hook.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

2. Register an Azure app

  1. Go to Azure Portal > App registrations > New registration
  2. Name: Sticky Notes Sync (or anything)
  3. Supported account types: Personal Microsoft accounts only
  4. Redirect URI: leave blank
  5. Click Register
  6. Go to Authentication > Add a platform > Mobile and desktop applications
  7. Check https://login.microsoftonline.com/common/oauth2/nativeclient > Configure
  8. Scroll to Advanced settings > set Allow public client flows to Yes > Save
  9. Go to API permissions > Add a permission > Microsoft Graph > Delegated permissions > add Mail.Read
  10. Copy the Application (client) ID from the overview page

3. Create a dedicated Gmail account

A Gmail app password grants full IMAP and SMTP access to the account β€” reading all mail and sending as the account owner. Do not use your primary Gmail for this. Create a throwaway one instead.

  1. In a fresh browser session, sign up at accounts.google.com/signup (e.g. yourname.stickynotes@gmail.com).
  2. Go to myaccount.google.com/security and enable 2-Step Verification (required before you can create app passwords).
  3. Don't use this account for anything else β€” no mail, no contacts, no signups. It exists purely as a sync relay.

4. Create a Gmail App Password

  1. Signed into the dedicated account, go to myaccount.google.com/apppasswords.
  2. Create a new app password named e.g. Sticky Notes Sync.
  3. Copy the 16-character password (spaces are cosmetic; either form works).

5. Configure

cp config.example.json config.json
chmod 600 config.json

Edit config.json:

{
    "gmail_address": "yourname.stickynotes@gmail.com",
    "gmail_app_password": "xxxxxxxxxxxxxxxx",
    "ms_client_id": "your-azure-app-client-id"
}

The chmod 600 restricts the file to your user only β€” other local accounts cannot read your app password.

6. iOS setup

On your iPhone:

  1. Go to Settings > Mail > Accounts > Add Account > Google
  2. Sign in with the dedicated Gmail account (using its regular password + 2FA code β€” not the app password; iOS uses OAuth for its own Mail client)
  3. On the toggle screen, turn Notes ON. Turn Mail/Contacts/Calendar OFF unless you want them.
  4. Optional: Settings > Notes > Default Account β€” keep as iCloud so your personal notes still default there.

Synced notes appear in the Notes app under a section labeled with the dedicated Gmail address.

7. Authenticate with Microsoft

source .venv/bin/activate
python sync_notes.py login

Follow the on-screen instructions to sign in via browser. The token is cached locally in ms_token_cache.json and auto-refreshes for ~90 days. Restrict this file too:

chmod 600 ms_token_cache.json

Usage

source .venv/bin/activate

# Sync notes once
python sync_notes.py

# List notes without connecting to IMAP
python sync_notes.py --list

# Preview what would sync (including deletions)
python sync_notes.py --dry-run

# Re-sync all notes, ignoring previous state
python sync_notes.py --force

# Run as a daemon, polling every 2 minutes
python sync_notes.py watch

# Custom polling interval (seconds)
python sync_notes.py watch --interval 60

# Re-authenticate with Microsoft
python sync_notes.py login

# Use local SQLite DB instead of Graph API (fallback)
python sync_notes.py --source local

How it works

  1. Authenticates with Microsoft via device code flow (MSAL).
  2. Reads Sticky Notes from the Graph API (/me/mailfolders/notes/messages).
  3. Compares against local sync state to find new, modified, and deleted notes.
  4. Pushes additions/updates to Gmail's IMAP Notes folder as RFC 2822 messages with the X-Uniform-Type-Identifier: com.apple.mail-note header. Each message carries an X-Sticky-Note-ID header used to locate it later for updates or deletion.
  5. Expunges the IMAP message for any note that has disappeared from Sticky Notes since the last sync.
  6. iOS automatically syncs the Gmail Notes folder to the Notes app.

Watch mode

The watch command uses Microsoft Graph delta queries to efficiently detect changes. Instead of fetching all notes each cycle, it asks the API "what changed since last time?" β€” when nothing changed, the response is nearly empty. A non-empty delta triggers a full sync, which picks up additions, edits, and deletions in one pass.

Running automatically on Windows

The windows/ directory contains scripts to register the watch daemon as a Windows Task Scheduler job so it starts on login.

  1. Open PowerShell as your normal user (not admin) and run:
    powershell -ExecutionPolicy Bypass -File windows\setup_task.ps1
  2. The task is named StickyNotesSync and runs windows\run_watch.vbs, which invokes the daemon inside WSL without a visible console.
  3. Logs are written to watch.log in the project root. The daemon auto-rotates this file when it exceeds 1 MB, keeping the most recent 500 lines (see Log output).

Useful management commands:

Start-ScheduledTask       -TaskName 'StickyNotesSync'    # run now
Get-ScheduledTask         -TaskName 'StickyNotesSync'    # check status
Unregister-ScheduledTask  -TaskName 'StickyNotesSync'    # remove

The windows\run_watch.bat file runs the same command with a visible console and is useful for debugging.

Health monitoring

Because the Task Scheduler job runs without a visible console, the daemon surfaces problems two ways:

1. heartbeat.json β€” written every poll cycle, inspectable any time:

{
    "last_success_ts": 1776560978.14,
    "error_state": false,
    "last_error": null,
    "last_error_ts": null,
    "last_toast_ts": null
}

If error_state is true or last_success_ts is older than a couple of poll intervals, the daemon is stuck.

2. Windows toast notifications β€” when the daemon hits an unrecoverable auth error (expired MS refresh token, revoked Gmail app password), it fires a Windows 10/11 toast titled "Sticky Notes Sync: login needed" naming the fix. Toasts are rate-limited to once per hour per error state so a dead credential doesn't spam you. On the first successful cycle after an error, you'll get a "Sticky Notes Sync recovered" toast.

If you see the login-needed toast, run:

source .venv/bin/activate
python sync_notes.py login

Log output

The daemon writes to watch.log via the Task Scheduler shell redirect. Two behaviors keep this log useful instead of noisy:

  • Auto-rotation. When watch.log crosses 1 MB (typically many months of runtime), the daemon trims it in place to the most recent 500 lines and prepends a --- log rotated at ... --- marker. No external logrotate or cron job is needed.
  • No-change cycles are suppressed in daemon mode. When stdout is redirected (Task Scheduler / VBS) the daemon is silent on cycles where nothing changed β€” heartbeat.json already records those. Interactive runs (run_watch.bat or a terminal) still print per-cycle status so you can see the daemon is alive.

Sync events, errors, and initial startup always log regardless of mode.

Security

Read this before committing anything or adopting this tool more widely.

Why a dedicated Gmail account matters

A Gmail App Password is not IMAP-only β€” it grants full IMAP and SMTP access. An attacker with this password can read all mail in the inbox and send mail as you. Using a dedicated account collapses that blast radius to an empty inbox and an identity nobody recognizes.

Files that must stay local

These are gitignored by default and blocked by the pre-commit hook β€” verify before any git add -A:

File Contains
config.json Gmail app password, Azure client ID
ms_token_cache.json Microsoft OAuth access + refresh tokens
sync_state.json Note IDs and last-modified timestamps
delta_link.json Opaque Graph delta cursor
heartbeat.json Daemon health state
watch.log Daemon log, may include note subjects

Restrict them to your user:

chmod 600 config.json ms_token_cache.json sync_state.json delta_link.json heartbeat.json watch.log

Pre-commit hook

scripts/pre-commit-hook.sh rejects commits that stage any of the files above, or whose diffs contain a gmail_app_password, access_token, or refresh_token value. Install on a fresh clone with:

cp scripts/pre-commit-hook.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

The hook turns a careless git add -A && git commit from a leak into a loud failure. Bypass with --no-verify only if you are absolutely certain it's a false positive.

Dependency pinning

requirements-lock.txt pins the full transitive dependency tree, not just msal. Use it for installs to reduce exposure to supply-chain compromises in indirect dependencies. Regenerate after deliberate upgrades with pip freeze > requirements-lock.txt.

Input validation

Note IDs from the Graph API are validated against a URL/base64-safe character set before they reach imap.search(), to prevent any future IMAP-search injection vector from malformed IDs. This is defense-in-depth β€” Microsoft issues the IDs, so no current exploit path exists.

If the repo is on GitHub

Keep it private, or audit the .gitignore carefully before every commit. A single accidental git add config.json published to a public repo will leak the app password within seconds to credential scrapers.

Disk encryption

Linux chmod 600 does not protect against reads from the Windows side via \\wsl$\Ubuntu\... β€” Windows uses its own ACLs there. On Windows, enable BitLocker on the system drive so an attacker with physical access cannot pull the disk out and read secrets offline. Single-user laptops are fine with default BitLocker; shared Windows machines want distinct Windows accounts per user as well.

Scopes and revocation

  • The Microsoft Graph scope is Mail.Read β€” read-only access to mail (Sticky Notes included). Microsoft does not offer a narrower scope for Sticky Notes specifically.
  • If the dedicated Gmail app password is ever exposed, revoke it at myaccount.google.com/apppasswords β€” this is near-instant and you can generate a replacement.
  • If the MS token cache is ever exposed, sign out of all sessions at account.microsoft.com to invalidate refresh tokens.

Recommended habits

  • Rotate the MS refresh token every ~60 days. Run python sync_notes.py login on a reminder. The refresh token itself is valid for roughly 90 days; proactive rotation shortens any window where a stolen token is usable.
  • Periodically check heartbeat.json. If last_success_ts is stale or error_state is true, the daemon needs attention even if you missed the toast.
  • Audit git log --diff-filter=A before making the repo public (if you ever consider doing so). The pre-commit hook prevents future accidents; it can't retroactively un-leak historical ones.

Local SQLite fallback

If you prefer not to use the Graph API, you can read directly from the local Sticky Notes database:

python sync_notes.py --source local

This reads from plum.sqlite in the Windows Sticky Notes app data folder. Note that the local DB may not reflect recent changes if the app hasn't flushed its write-ahead log β€” the Graph API source is more reliable.

About

One-way sync tool for Windows Sticky Notes to iOS Notes app via Microsoft Graph API and Gmail IMAP. Developed with Claude Code.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors