惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
www.infosecurity-magazine.com
www.infosecurity-magazine.com
大猫的无限游戏
大猫的无限游戏
爱范儿
爱范儿
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threatpost
V
Visual Studio Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - Franky
人人都是产品经理
人人都是产品经理
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Cloudflare Blog
N
News and Events Feed by Topic
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
AWS News Blog
AWS News Blog
S
SegmentFault 最新的问题
T
Tailwind CSS Blog
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Spread Privacy
Spread Privacy
J
Java Code Geeks
博客园 - 聂微东
T
Tor Project blog
宝玉的分享
宝玉的分享
博客园 - 叶小钗
Webroot Blog
Webroot Blog
博客园 - 【当耐特】
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
H
Heimdal Security Blog
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
I
InfoQ
Security Latest
Security Latest
Martin Fowler
Martin Fowler
Hacker News: Ask HN
Hacker News: Ask HN
P
Privacy International News Feed
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
雷峰网
雷峰网
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
H
Help Net Security
L
LINUX DO - 最新话题
L
LINUX DO - 热门话题

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Sync a mailbox into your own database with Nylas
Qasim · 2026-06-25 · via DEV Community

You want your app to have its own copy of a user's email: to search it fast, run analytics over it, or feed it to a model without hitting the provider on every query. The naive version is a one-time pull that's stale the moment it finishes, because new mail keeps arriving and old mail gets read, moved, and deleted. A real sync is two jobs working together: backfill the history once, then track changes forever after. This post builds that out with the Email API and shows the CLI shortcuts for testing each piece.

It's a worked use case rather than an endpoint tour, pulling together list messages, cursor pagination, and the message webhooks from two angles: the HTTP API your backend runs and the nylas CLI for the terminal. I work on the CLI, so the commands below are the ones I reach for when I'm checking what a sync will actually see.

The two phases of a mailbox sync

A durable sync splits into a backfill and a tail. The backfill is a one-time walk through every message already in the mailbox, page by page, writing each into your database. The tail is the ongoing part: webhooks tell you about each new message, each change, and each deletion as it happens, so your copy keeps pace with the real mailbox instead of drifting from it.

The reason to separate them is that they have different shapes. Backfill is a bounded loop over a known, finite set, optimized for throughput, and you run it once per grant. The tail is an unbounded event stream optimized for latency, and it runs for the life of the connection. A mailbox with 40,000 messages takes a while to backfill but only has to be done once, while the tail handles the handful of changes that trickle in every minute after. Build them as two code paths that write to the same store.

Phase 1: backfill with cursor pagination

The backfill walks the mailbox in pages. A GET /v3/grants/{grant_id}/messages call returns a page of messages plus a next_cursor in the response. You pass that cursor back as the page_token query parameter on the next call, and you keep going until a response comes back without a next_cursor, which means you've reached the end.

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages?limit=50&page_token=<CURSOR>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"

The limit parameter sets the page size. If you start hitting 429 or provider rate limits while listing, the docs suggest dropping limit to 20 and adding query parameters to narrow the results. The loop is simple: call, write the page to your database, read next_cursor, repeat. Persist the cursor between calls rather than holding it only in memory, because that's what makes the backfill resumable if the process restarts midway, which on a large mailbox it eventually will.

Back up a mailbox from the CLI

Before writing the backfill loop, it helps to see what it'll pull, and the CLI does the pagination for you. nylas email list --all walks the mailbox with pagination handled internally, the inbox only by default, and --max caps the total so a test run doesn't walk everything. Add --json to get structured output you can pipe into a file or jq.

# Pull up to 500 messages across all folders as JSON
nylas email list --all --max 500 --all-folders --json > backup.json

By default the command only lists the inbox, so --all-folders is what makes it a real backup rather than an inbox dump. This is the fast way to answer practical questions before you build anything: how many messages are in this mailbox, what the payload looks like, and roughly how long a full walk takes. The --all flag uses the same cursor pagination underneath that your backend loop will, so what you see here is what your sync will get.

Phase 2: stay in sync with webhooks

Once the history is in, the tail keeps your copy current, and it's event-driven rather than polled. You subscribe to three message triggers and act on each. message.created fires when mail arrives, so you insert it. message.updated fires when a message changes, read, starred, or moved, so you update your row. And message.deleted fires when one is removed, so you delete or tombstone yours.

app.post("/webhooks/nylas", async (req, res) => {
  res.sendStatus(200); // acknowledge fast
  const { type, data } = req.body;
  const msg = data.object;
  if (type.startsWith("message.deleted")) {
    await db.deleteMessage(msg.id);
  } else if (type.startsWith("message.created") || type.startsWith("message.updated")) {
    // Payloads over 1 MB arrive as ".truncated" with the body removed; re-fetch it.
    const full = type.endsWith(".truncated") ? await fetchMessage(msg.id) : msg;
    await db.upsertMessage(full);
  }
});

Acknowledge the delivery fast with a 200 and do the database write after, so a slow query never causes a webhook timeout and redelivery. Two details make the matching prefix-based rather than exact. A notification over 1 MB arrives with a .truncated suffix on the type and its body field stripped, so you re-query the message to get the full content. And if you turn on field customization, the type gains a .transformed suffix. Created and updated both do the same upsert, since keying on the message ID means a create and a later update land on the same row, which is exactly the idempotency the next section is about. The deletion path is the one most people forget, and skipping it leaves your copy showing mail the user already trashed.

Make every write idempotent

The single rule that keeps a sync correct is that every write is keyed on the message ID, so doing it twice is the same as doing it once. This matters because the two phases overlap and webhooks repeat. A message can arrive over a webhook while your backfill is still running and pull the same one, and providers redeliver webhooks, so the same message.created can fire more than once. Without idempotency, you get duplicate rows for one email.

An upsert solves all of it. When you write a message, insert it if the ID is new and overwrite if the ID exists, rather than blindly inserting. Then the backfill and the tail can both touch the same message in any order with no duplicates, a redelivered webhook is harmless, and re-running a failed backfill page just rewrites rows you already had. The message ID that every endpoint and webhook returns is what you key on, so make it the primary key of your table. Get this one detail right and most of the scary concurrency in a sync stops mattering.

Resume a backfill with a time window

A backfill over a huge mailbox shouldn't be one unbroken loop, because any failure midway means starting over. Two query parameters let you slice it into resumable windows: received_after and received_before each take a Unix timestamp and bound the results to a date range. You walk the mailbox a month at a time, record which windows are done, and a crash only costs you the window in flight.

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages?received_after=1704067200&received_before=1706745600&limit=50" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"

Windowing also lets you prioritize. Sync the last 90 days first so the app has recent mail to show immediately, then backfill the older history in the background where nobody's waiting on it. Date windows are an API feature; the CLI doesn't expose date flags, but nylas email list --from <sender> and --folder <name> scope a test pull to a slice of the mailbox when you want to inspect one sender or folder rather than the whole thing. The combination of cursor pagination within a window and date windows across the mailbox is what turns a fragile one-shot job into one you can stop and resume at will.

Store the right amount, not everything

A message body can be large, and a mailbox has thousands of them, so storing every full body is often the wrong default. The list response includes the body by default, but the select query parameter trims it to just the fields you name, so a metadata-only backfill stays lean. You store what you query on, the ID, sender, subject, date, folders, and read state, and fetch the full body from GET /v3/grants/{grant_id}/messages/{message_id} on demand when a user opens a message.

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages?select=id,from,subject,date,folders,unread&limit=50" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"

With select narrowing the payload, your database stays small and fast for the list and search views that run constantly, and the heavy body is pulled only for the one message someone's reading.

Which way you lean depends on the use case. If you're running full-text search or feeding a model, you need the bodies, so store them, ideally in a store built for large text rather than your primary transactional table. If you're building an inbox list view or analytics over headers, metadata alone is plenty and far cheaper to keep current. Decide up front, because backfilling bodies later means a second walk through the whole mailbox.

Where a local sync pays off

This pattern shows up under a lot of different products, and the mechanism is the same each time. A few that map straight onto it:

  • Fast search and filtering. Querying your own indexed database returns results in milliseconds, where hitting the provider per query is slow and rate-limited. The sync is what makes instant search possible.
  • Analytics and reporting. Response times, volume by sender, thread activity, all of it runs over your local copy without touching the mailbox, so a heavy report never competes with live traffic.
  • AI and retrieval. Feeding email to a model for summarization or retrieval needs the content sitting in a store you control, not fetched live on every prompt.
  • Offline and resilience. Your app keeps working through a provider hiccup because the data it reads is local, and the tail catches up when the connection returns.

Each is the same two-phase build, backfill then tail, with the same idempotent writes underneath. What changes is how much of each message you keep and what you index, not the sync itself.

Things to keep in mind

A short list of details keeps a mailbox sync correct under load.

  • Key every write on the message ID. Idempotent upserts are what make overlapping backfill and webhook writes safe; without them you get duplicate rows.
  • Handle message.deleted, not just created. Skipping deletions leaves your copy showing mail the user already removed, which reads as a bug.
  • Persist the pagination cursor. Storing next_cursor between pages is what makes a backfill resumable after a restart instead of starting over.
  • Window large backfills by date. received_after and received_before slice the mailbox into resumable chunks and let you sync recent mail first.
  • Acknowledge webhooks before the database write. Return 200 fast, then process, so a slow query doesn't trigger a redelivery storm.
  • Store metadata, fetch bodies on demand. Keep your hot table small and pull the full body only when a message is opened, unless search or AI needs the text.

Wrapping up

A mailbox sync is a backfill plus a tail. Walk the history once with cursor pagination, following next_cursor until it runs out, and slice it into date windows so it's resumable. Then keep it current with the message.created, message.updated, and message.deleted webhooks, writing every change as an upsert keyed on the message ID so overlap and redelivery never duplicate a row. Test the walk from the terminal with nylas email list --all, decide whether you need full bodies or just metadata, and you have a local copy that stays honest with the real mailbox.

Where to go next: