Skip to content

mjasion/imap2gmail

Repository files navigation

imap2gmail

Pull mail from any IMAP mailbox (e.g. o2.pl / wp.pl) into Gmail — and, crucially, have it go through Gmail's spam filter. A small Go binary that runs on a schedule (AWS Lambda, or anything with cron).

A self-hosted replacement for Gmail's removed "Check mail from other accounts" (POP3) feature, for mailboxes whose provider charges for server-side forwarding.

Why this exists

  • Google is retiring the POP3-based "Check mail from other accounts" and Gmailify (rollout from early 2026). If your Gmail used to fetch your o2/wp mail and "suddenly stopped," that's usually why — not your provider.
  • o2.pl / wp.pl (Wirtualna Polska group) allow IMAP/POP3 for free, but automatic forwarding to an external address is a paid feature ("Poczta bez reklam" / PRO).
  • So: pull the mail yourself over IMAP and deliver it into Gmail.

How it works

EventBridge cron ──▶ Lambda ──▶ IMAP pull (UNSEEN, BODY.PEEK[])  ──▶ Gmail API
                                  from poczta.o2.pl:993                messages.import
                                                                       (spam filter ON)
                                  ◀── mark \Seen only after a successful import

The key detail: it delivers via the Gmail API users.messages.import, not insert / IMAP APPEND. Per Google's docs, import runs the message through "standard email scanning and classification similar to receiving via SMTP" — so Gmail's spam classifier and filters apply (neverMarkSpam=false). insert and IMAP APPEND bypass classification, which is why Thunderbird/imapsync copies never get spam-filtered.

Messages are flagged \Seen on the source only after a successful import, so the run is safe to retry and never silently drops mail. BODY.PEEK[] ensures fetching never marks mail read prematurely.

Features

  • Multiple source accounts → one or many Gmail destinations, in one config.
  • Spam filtering preserved (Gmail API messages.import).
  • Idempotent & crash-safe (\Seen set post-import).
  • Single static binary, ARM64, ~0 cold-start deps. Runs on Lambda free tier.
  • Config via AWS Secrets Manager, an env var, a file, or single-account env vars.
  • Tested against an in-memory IMAP server.

Requirements

  • Go 1.26+ (to build / run cmd/auth).
  • A Google Cloud project with the Gmail API enabled.
  • IMAP access on the source mailbox (+ an app password).
  • For deploy: AWS account + AWS SAM CLI.

Setup

1. Enable IMAP + app password on the source (o2 / wp)

In webmail: Opcje → "Dostęp dla programów pocztowych" (IMAP/POP), enable it, and generate an app password (hasło aplikacji) if 2FA is on. Provider settings:

Provider IMAP host Port TLS
o2.pl poczta.o2.pl 993 implicit
wp.pl imap.wp.pl 993 implicit

First sanity check: make sure you can log into the mailbox over IMAP at all (e.g. from Thunderbird). If the provider blocks IMAP on free accounts too, this tool can't help and paid forwarding is the only option.

2. Create a Gmail OAuth client

  1. Google Cloud Console → new project → enable the Gmail API.
  2. OAuth consent screen → User type External → add yourself as a test user.
  3. Publish the app to "In production" (click through the "unverified app" warning). This matters — see the 7-day pitfall.
  4. Credentials → Create OAuth client ID → Desktop app → copy the client ID and secret.

3. Generate a refresh token

GOOGLE_CLIENT_ID=xxxx.apps.googleusercontent.com \
GOOGLE_CLIENT_SECRET=GOCSPX-xxxx \
make auth

A browser opens; approve access. This writes token.json containing client_id, client_secret, refresh_token. Keep it secret.

4. Configure

Copy config.example.json to config.json and fill in the IMAP creds and the gmail.token object (from token.json). You can list several accounts.

Run locally

CONFIG_FILE=config.json go run .

or single-account mode with env vars:

IMAP_HOST=poczta.o2.pl IMAP_USER=you@o2.pl IMAP_PASSWORD='app-pass' \
GMAIL_TOKEN_JSON="$(cat token.json)" go run .

It prints a JSON summary of what was imported and exits non-zero on error.

Deploy to AWS Lambda

The SAM template stores your config in Secrets Manager and schedules the function with EventBridge.

make deploy CONFIG_JSON="$(cat config.json)"

(make deploy builds the arm64 binary, then sam deploy --guided.)

Change the polling interval with the ScheduleExpression parameter, e.g. rate(5 minutes) or cron(0/15 * * * ? *).

Or via GitHub Actions

.github/workflows/deploy.yml deploys on manual dispatch. Set repo secrets:

  • AWS_ROLE_ARN — an IAM role to assume via OIDC,
  • CONFIG_JSON — your config JSON on a single line (minified).

.github/workflows/ci.yml runs vet + tests + build on every push/PR.

Configuration reference

Resolved in priority order:

  1. CONFIG_SECRET_ARN — JSON read from AWS Secrets Manager (Lambda default).
  2. CONFIG_JSON — JSON inline in an env var.
  3. CONFIG_FILE — path to a JSON file.
  4. Single-account env vars (below).

Single-account env vars: IMAP_HOST (default poczta.o2.pl), IMAP_PORT (993), IMAP_USER, IMAP_PASSWORD, IMAP_MAILBOX (INBOX), GMAIL_USER (me), GMAIL_TOKEN_JSON (full token JSON), ACCOUNT_NAME.

JSON schema: see config.example.json. Per account: name, imap{host,port,user,password,mailbox}, gmail{user,token{client_id, client_secret,refresh_token}}, optional markSeen (default true).

⚠️ The 7-day OAuth pitfall

If your Google OAuth app stays in "Testing" publishing status, refresh tokens for Gmail (a restricted scope) expire after 7 days and the tool breaks weekly. Fixes:

  • Publish the app to "In production" (accept the unverified-app warning for personal/single-user use) → refresh tokens are long-lived. ✅ Recommended.
  • Or use a Google Workspace "Internal" app (no expiry, no verification).
  • Or stay in Testing and re-run make auth every week (annoying).

Full CASA verification is only needed to distribute the app publicly — not for your own mailbox.

Security notes

  • Secrets (IMAP app password, Gmail refresh token) belong in Secrets Manager or GitHub Actions secrets — never commit them. .gitignore already excludes token.json, config.json, client_secret.json, .env.
  • Only the gmail.insert scope is requested (add mail; no read access to your existing Gmail).
  • The Lambda's IAM policy is limited to reading the one config secret.

Limitations

  • Pulls INBOX (configurable per account) — not all folders.
  • Polling, not push: new mail appears within one schedule interval.
  • One-time migration of an existing backlog is out of scope — use imapsync / Thunderbird for that (those use IMAP APPEND, so no spam filtering, which is fine for already-triaged old mail).

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors