This repo contains a small IMAP-to-Gmail mail mover. It pulls new messages from one or more source IMAP mailboxes and imports them into Gmail labels through the Gmail API. After a successful import, the source message is deleted and expunged. Each poll scans the current source folder and moves whatever is still present.
- Create a Google Cloud project and enable the Gmail API
- Create OAuth Desktop App credentials and save the JSON file
- Put the credentials JSON somewhere readable and reference it from
config.ini - Run the OAuth flow once so a token JSON is created for that Gmail account
(for headless servers, use
oauth_mode=consoleand--authorize-only)
If you want replies to use custom From addresses (info@..., support@..., etc.):
- Gmail Settings -> Accounts and Import -> "Send mail as"
- Add each address
- Set "Send through your SMTP server" (MXroute SMTP)
- Near real-time inbound sync from source IMAP to Gmail
- Deletes from source only after Gmail import succeeds
- Creates destination labels if
create_labels=yes - Supports multiple Gmail profiles and multiple source mailboxes
- Preserves Seen/Unseen and an original-ish timestamp
- Sync sent mail (send from Gmail instead)
- Keep source-side history after it has been moved
config.ini
- General section controls polling interval and log level.
gmail_*sections define each Gmail destination profile.src_*sections define each source mailbox and its destination mapping.
config.sample.ini
- A fully commented template you can copy to create your own
config.ini.
secrets.env
- Holds source mailbox passwords as environment variables.
config.inireferences them viasource_password_envto avoid storing plain text.
*.json
- Holds Google OAuth client credentials and per-account token files.
- Token files are refreshed automatically when possible.
invalid-token-alert-state.txt
- Optional state file used to rate-limit invalid-token alert emails to once per day.
General ([general])
poll_seconds: Poll interval in seconds.log_level:DEBUG,INFO,WARNING, orERROR.env_file: Optional env file to load at startup. If relative, it is resolved from theconfig.inidirectory. Defaults tosecrets.env. For hardened systemd installs, prefer/var/lib/mailfetcher/secrets.env.create_labels:yes/noto auto-create destination Gmail labels.invalid_token_alert_enabled:yes/noto send a notification email when a Gmail token refresh fails because the token is no longer valid.invalid_token_alert_to: Recipient for invalid-token alerts, such asjplebel@google.com.invalid_token_alert_source_section: Optionalsrc_*section to reuse for alert delivery. When set, MailAggregator uses that source account's username and stored password for SMTP.invalid_token_alert_from: Sender address for invalid-token alerts. Defaults tomailfetcher@localhost.invalid_token_alert_state_file: File used to remember whether an alert has already been sent today. Defaults to/var/lib/mailfetcher/invalid-token-alert-state.txtwhenconfig.iniis under/etc/mailfetcher, otherwiseinvalid-token-alert-state.txtrelative toconfig.ini.invalid_token_alert_smtp_host: SMTP relay hostname for alert emails. Defaults tolocalhost.invalid_token_alert_smtp_port: SMTP relay port for alert emails. Defaults to25.invalid_token_alert_smtp_starttls:yes/noto enable STARTTLS before sending alert email.invalid_token_alert_smtp_username: Optional SMTP username for alert emails.invalid_token_alert_smtp_password_env: Optional env var containing the SMTP password for alert emails.invalid_token_alert_smtp_password: Optional plain-text SMTP password for alert emails (not recommended).
Gmail profile ([gmail_*])
username: Gmail address (used for identification/logging).credentials_file: Required OAuth client JSON path. If relative, it is resolved from theconfig.inidirectory.token_file: Required OAuth token JSON path. If relative, it is resolved from theconfig.inidirectory.oauth_mode:local_server(browser-based) orconsole(copy/paste code in terminal).- For hardened systemd installs, prefer:
credentials_file = /etc/mailfetcher/credentials.jsontoken_file = /var/lib/mailfetcher/token-<profile>.json
Source mailbox ([src_*])
source_host: Source IMAP host.source_port: Source IMAP SSL port.source_username: Source mailbox user.source_password_env: Env var name holding the source password.source_password: Optional plain-text fallback (not recommended).source_smtp_host: Optional SMTP host for sending alerts or outbound mail. Defaults tosource_host.source_smtp_port: Optional SMTP port for sending alerts or outbound mail. Defaults to587.source_smtp_starttls:yes/noto enable STARTTLS for the source account's SMTP connection. Defaults toyes.source_smtp_from: Optional sender address for SMTP mail. Defaults tosource_username.source_folder: Folder to pull from (defaultINBOX).dest_profile: Gmail profile name to receive mail.dest_folder: Comma-separated list of Gmail labels to apply. If empty, defaults to the source email address. Include\Inboxto force a message to appear in Inbox. System labels (like\Inbox) are not auto-created.
Create system user and directories:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin mailfetcher || true
sudo mkdir -p /etc/mailfetcher
sudo mkdir -p /var/lib/mailfetcher
sudo chown -R mailfetcher:mailfetcher /var/lib/mailfetcher
sudo chmod 700 /var/lib/mailfetcher
Create Python virtual environment and install dependencies:
sudo python3 -m venv /var/lib/mailfetcher/.venv
sudo /var/lib/mailfetcher/.venv/bin/pip install --upgrade pip
sudo /var/lib/mailfetcher/.venv/bin/pip install -r requirements.txt
sudo chown -R mailfetcher:mailfetcher /var/lib/mailfetcher/.venv
Install app:
sudo cp mailfetcher.py /usr/local/bin/mailfetcher.py
sudo chmod 755 /usr/local/bin/mailfetcher.py
sudo chown root:root /usr/local/bin/mailfetcher.py
Install config:
sudo cp config.ini /etc/mailfetcher/config.ini
sudo chown root:mailfetcher /etc/mailfetcher/config.ini
sudo chmod 640 /etc/mailfetcher/config.ini
Install Gmail OAuth files:
sudo cp credentials.json /etc/mailfetcher/credentials.json
sudo chown root:mailfetcher /etc/mailfetcher/credentials.json
sudo chmod 640 /etc/mailfetcher/credentials.json
# Create/write token location (must be writable by service user)
sudo touch /var/lib/mailfetcher/token_jpl.json
sudo chown mailfetcher:mailfetcher /var/lib/mailfetcher/token_jpl.json
sudo chmod 600 /var/lib/mailfetcher/token_jpl.json
Bootstrap OAuth token (headless server):
# In /etc/mailfetcher/config.ini under [gmail_*], set:
# oauth_mode = console
sudo -u mailfetcher -H /var/lib/mailfetcher/.venv/bin/python \
/usr/local/bin/mailfetcher.py --authorize-only /etc/mailfetcher/config.ini
Install secrets:
sudo cp secrets.env /var/lib/mailfetcher/secrets.env
sudo chown mailfetcher:mailfetcher /var/lib/mailfetcher/secrets.env
sudo chmod 600 /var/lib/mailfetcher/secrets.env
Install service unit:
sudo cp mailfetcher.service /etc/systemd/system/mailfetcher.service
sudo systemctl daemon-reload
Service runtime:
ExecStartruns/var/lib/mailfetcher/.venv/bin/pythonso Gmail API dependencies come from the service venv.
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now mailfetcher.service
Logs:
sudo journalctl -u mailfetcher.service -f
Run:
docker compose up -d --build
docker compose logs -f
Stop:
docker compose down
- Keep
secrets.envatchmod 600. - Keep OAuth token files in
/var/lib/mailfetcheratchmod 600. - Keep
credentials.jsonin/etc/mailfetcheratchmod 640. - Prefer env var references in
config.iniover plain text secrets.
- Authentication failures: Confirm the Gmail API is enabled, the OAuth credentials path is correct, and the token file matches the target account.
ModuleNotFoundError: No module named 'google'injournalctl: the service is using a Python interpreter without dependencies. Recreate/update/var/lib/mailfetcher/.venvand ensuremailfetcher.serviceuses/var/lib/mailfetcher/.venv/bin/python.could not locate runnable browserinjournalctl: OAuth is trying browser mode on a headless server. Setoauth_mode=console, run--authorize-onlyonce interactively to generate the token, then restart the service.Error 400: invalid_requestwithMissing required parameter: redirect_uri: your OAuth client JSON is missing valid redirect URIs (or is the wrong client type). Use Google OAuth Desktop App credentials and updatecredentials_fileinconfig.ini.- No messages moved: Check that
source_folderis correct and that another client is not moving or deleting messages before MailAggregator sees them. - Messages not appearing in Gmail: Verify the
dest_profileanddest_foldervalues and that the destination Gmail account is authorized. - Frequent retries: Network instability or provider rate limits can cause backoff retries. Check source IMAP connectivity and Gmail API access from the host.