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.
- 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.
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.
- Multiple source accounts → one or many Gmail destinations, in one config.
- Spam filtering preserved (Gmail API
messages.import). - Idempotent & crash-safe (
\Seenset 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.
- 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.
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.
- Google Cloud Console → new project → enable the Gmail API.
- OAuth consent screen → User type External → add yourself as a test user.
- Publish the app to "In production" (click through the "unverified app" warning). This matters — see the 7-day pitfall.
- Credentials → Create OAuth client ID → Desktop app → copy the client ID and secret.
GOOGLE_CLIENT_ID=xxxx.apps.googleusercontent.com \
GOOGLE_CLIENT_SECRET=GOCSPX-xxxx \
make authA browser opens; approve access. This writes token.json containing
client_id, client_secret, refresh_token. Keep it secret.
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.
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.
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 * * * ? *).
.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.
Resolved in priority order:
CONFIG_SECRET_ARN— JSON read from AWS Secrets Manager (Lambda default).CONFIG_JSON— JSON inline in an env var.CONFIG_FILE— path to a JSON file.- 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).
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 authevery week (annoying).
Full CASA verification is only needed to distribute the app publicly — not for your own mailbox.
- Secrets (IMAP app password, Gmail refresh token) belong in Secrets Manager
or GitHub Actions secrets — never commit them.
.gitignorealready excludestoken.json,config.json,client_secret.json,.env. - Only the
gmail.insertscope is requested (add mail; no read access to your existing Gmail). - The Lambda's IAM policy is limited to reading the one config secret.
- 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 IMAPAPPEND, so no spam filtering, which is fine for already-triaged old mail).
MIT — see LICENSE.