Privacy-first, local-first email triage for job seekers. mailwarden reads your Gmail read-only. It makes sure mail about your job applications is never missed, ranks the jobs in your job-alert emails against your own profile, summarises everything else into a digest, and shows it all on a local dashboard.
- Sensitive mail never reaches an AI. A local gate catches OTPs, bank/UPI/card mail, password resets, login alerts and ID/KYC/tax mail before any model sees anything.
- Free to run. Classification uses Groq's free tier (
openai/gpt-oss-20b), which receives only redacted text of non-sensitive mail. Alternatively, a fully local Ollama backend keeps everything on your machine. - Nothing useful on disk. OAuth tokens and keys live only in your OS keyring. The database is SQLCipher-encrypted and stores no email bodies or subjects.
- Local only. The dashboard listens on 127.0.0.1 and loads no external assets. Its only script is one first-party file for keyboard shortcuts, and every page works without it.
SECURITY.md lists exactly what leaves your machine, to which host, and what is stored.
Gmail (read-only) → sender rules → sensitivity gate → redaction → classify (LLM or rule)
→ encrypted store → Urgent · Applications · Job alerts · Digest · notifications
- Sender rules put senders in tiers:
priority(job mail),sensitive(banks, payments, government, account security),ignoreandjob_alert(job boards). - The sensitivity gate checks every message locally. Sensitive mail is held back: only the sender name, the time and a friendly reason are kept. Held-back job mail still shows up as "Company · stage · needs your attention".
- Redaction removes addresses, phone numbers, digit runs, links and secrets from the rest. Social notifications, job-board alerts and bulk mail are labelled by rule, with no LLM call.
- The classifier labels each remaining email (job, personal, newsletter ...). It also extracts the company, role, stage and deadline. Job mail updates your Applications.
- Job alerts are split into individual jobs and de-duplicated across sites. Each job is filtered and scored against your profile, and the best ones appear in Apply today.
- Python 3.11+ (3.13 recommended) and uv
- macOS, Linux or Windows with a working OS keyring: macOS Keychain, Secret Service or KWallet on Linux, or Windows Credential Manager
- A Google account and a free Google Cloud project for your own OAuth client
- A free Groq API key, or a local Ollama
The desktop app window and the app's own notifications are macOS-only. Everything else also runs on Linux and Windows, but has mainly been tested on macOS.
make install # .venv from hash-pinned requirements-dev.lock
.venv/bin/mailwarden init # prints the mailwarden home folderinit creates the private mailwarden home (mode 700) with config.toml and
sender_rules.yaml (mode 600). The home is:
- macOS:
~/Library/Application Support/mailwarden - Linux:
~/.config/mailwarden - Windows:
%LOCALAPPDATA%\mailwarden\mailwarden
Set MAILWARDEN_HOME to use another folder. mailwarden refuses to start if these
files are readable by other users.
mailwarden ships no shared client ID, so each user creates their own.
- In https://console.cloud.google.com/, create a project (e.g.
mailwarden). - APIs & Services → Library: enable the Gmail API.
- OAuth consent screen (Google Auth Platform):
- User type: External. App name: anything. Support email: your own address.
- Data access / Scopes: add only
https://www.googleapis.com/auth/gmail.readonly. - Audience → Test users: add every Gmail address you will connect.
- Credentials → Create credentials → OAuth client ID, type Desktop app. Download the JSON.
.venv/bin/mailwarden add-account --provider gmail --name personal \
--client-secrets ~/Downloads/client_secret_XXXX.jsonThe client is copied into the keyring, so delete the JSON file afterwards. Later
accounts don't need --client-secrets. Google warns that the app is unverified. This
is expected for a personal client, so choose Continue. mailwarden requests only
gmail.readonly. A token with any broader scope is revoked and refused.
Seven-day sign-ins. While the consent screen is in Testing, Google expires
refresh tokens after 7 days, and mailwarden then can't read the account. Every
dashboard page shows a banner and you get one notification a day; run
mailwarden reauth <name> to sign in again (all data is kept). To stop it happening
every week, set the publishing status to In production without submitting for
verification. That is allowed for personal use (fewer than 100 users). You will still
see the "unverified app" warning when you sign in.
Groq (default, free):
- Sign up at https://console.groq.com. Don't add a billing method: the account then stays on the free tier and can't be charged.
- In Settings → Data Controls, turn on Zero Data Retention.
- In API Keys, choose Create API key, then run:
.venv/bin/mailwarden set-groq-key # hidden prompt; stored in the OS keyring
.venv/bin/mailwarden doctor --llm # tests the key with a built-in fake emailOllama (fully local alternative):
- Install Ollama.
- Run
ollama pull qwen2.5:3b. - Set
[llm] backend = "ollama"inconfig.toml.
Nothing then leaves your machine for classification or scoring.
.venv/bin/mailwarden doctor --sync # keyring, permissions, scopes, message count
.venv/bin/mailwarden init --reset-rules # only if your sender_rules.yaml predates job_alert
.venv/bin/mailwarden dry-run --last 50 --summary
.venv/bin/mailwarden run # first real sync (backfills the last 7 days)For each message, dry-run shows the tier, the gate decision and the exact redacted
text that would go to the LLM. It stores nothing and notifies no one. It sends
nothing either, unless you add --with-llm.
.venv/bin/mailwarden schedule # writes files to <home>/schedule/<platform>/ and prints install commands- macOS: launchd agents sync every 10 minutes, build the digest at your digest
times and keep the dashboard running. They use
StartCalendarInterval, so a run missed while the laptop sleeps happens on wake (cron would skip it). - Linux: systemd user timers with
Persistent=true. - Windows: Task Scheduler XML with "run as soon as possible after a missed start".
Logs go to <home>/logs/ (mode 600). Each sync also fetches the job descriptions it
can fetch automatically, and scores new jobs.
.venv/bin/mailwarden launcher # macOS: creates ~/Applications/mailwarden.appDrag mailwarden.app to the Dock, or press ⌘Space and type "mailwarden". The app
opens the dashboard in its own window (macOS WebKit, not a browser) and signs you in
each time.
On the first launch, macOS asks whether "mailwarden-python" may use your Keychain. Enter your Mac login password and choose Always Allow.
Without the app, you have two options:
mailwarden dashboard --openruns the dashboard in the foreground.mailwarden openworks when the background agent is running. It opens a one-time sign-in link in your browser.
Open the app and go through these views:
- Urgent: job mail that needs you, soonest deadline first. It includes held-back job mail, which you open in Gmail because mailwarden deliberately didn't read it. Click Done when an item is handled. Nothing leaves Urgent until you do.
- Apply today: your best-matching new jobs, with the project to lead with and the skills you're missing.
- Digest: everything else since the last digest, by category, one line each.
- Applications: where each application stands.
Reload (⌘R) to see new items. The background sync runs every 10 minutes.
| View | What it shows |
|---|---|
| Urgent | Job items that need action, by deadline. Held-back job mail shows the company, the stage and a friendly reason ("Contains a verification code"), never the subject. Items are pinned: only Done removes them, even if they are reclassified later. Every item has an Open in Gmail link. |
| Applications | Companies by stage (applied, assessment, interview, offer, rejection), with each application's history. |
| Job alerts | Every job from your job-alert emails, in three views (see the next table). |
| Digest | The latest digest. Newsletters and job alerts are collapsed, and sensitive mail appears only as counts by sender. |
| Sensitive | Counts by sender only. |
| Settings | Read-only: the classifier, the allowed outbound hosts, accounts and sender tiers. |
Job alerts has three views:
| View | What it's for |
|---|---|
| Today | The daily shortlist (Apply today), in tiers. Tier A ("tailor + referral", tier_a_count, default 5) needs a full score of 7+; when it's short, the jobs most worth a job description are listed under it. Tier B ("quick apply", tier_b_count, default 10), and the rest under More. Shows "Applied X of Y today". |
| Review | Jobs whose experience level the alert doesn't state, one at a time: Fresher OK, Too senior, Closed, Dismiss, Applied, Paste JD. After each action the next job appears. Keys: F, S, C, D, A, and J / K for next / previous. |
| Browse | Everything else, filtered by status (Candidates, Filtered, Expired, All) and a sidebar: source type, source (top 5, more, and search), stack family, experience, score type and location. Filters live in the URL, so a view can be bookmarked. |
Job rows are one line (score badge, title, company, location, source, age, family and
experience chips); click a row to see why it scored as it did, the project to lead
with, missing skills, and the paste box. A solid badge is a full score, an outlined
one is preliminary. Open and Applied are always visible; the rest is in the
… menu. "How ranking works" explains the order. /apply and old Job alerts links
still work.
Every account you add appears on the same dashboard. Sign-in uses a one-time link that is valid for 10 minutes. A cookie then keeps you signed in for 90 days.
Jobs are scored against a profile that is built locally from your own files. The files live in the private mailwarden home, never in the repo:
.venv/bin/mailwarden profile init # creates <home>/profile/ and profile/projects/
# put your CV (PDF) in <home>/profile/ and one Markdown file per project in profile/projects/
chmod 600 <home>/profile/*.pdf <home>/profile/projects/*.md
.venv/bin/mailwarden profile build --dry-run # preview the diff
.venv/bin/mailwarden profile build # write <home>/profile.yaml (previous kept as .bak)profile.yaml holds weighted skills, years of experience, seniority, target and
avoid roles, locations and your projects. Only the project files count as project
evidence. The Projects section of your CV is ignored.
You can edit the file by hand. A rebuild replaces only the generated blocks
(skills, experience_years, projects). Every other line stays exactly as you
wrote it, comments included. These sections are yours to edit:
skill_overrides: {skill: weight}replaces automatic weights, and adds skills the parser missed. Scores use the automatic weights with these overrides applied.extra_project_skills: {project name: [skills]}adds skills to that project on every rebuild.buildwarns if a name matches no project file.education,experience_summaryandhighlightsare free text given to the scorer.target_rolesentries may list alternatives separated by/, e.g.sde / sde-1 / sde i. They highlight matching titles on the Job alerts page.- Filtering (roles to avoid, locations, experience) lives in
matching.yaml, not here.mailwarden matching checktells you if oldavoid_roles/locationsentries are still inprofile.yaml.
profile.yaml is given to the LLM for scoring, so it must never contain contact
details. build refuses to write the file if it would include an email address, a
phone number, a link or your name.
<home>/config/matching.yaml is the single place for matching rules: experience
levels, hard exclusions, soft penalties, stack families with an evidence project each,
skills to ignore when picking a project, and the scoring rubric given to the model.
profile.yaml keeps who you are (skills, projects, education, highlights).
.venv/bin/mailwarden matching init # create it from the template (never overwrites)
.venv/bin/mailwarden matching check # validate it and list likely mistakesParsing is strict, so a misspelt key is an error rather than a silently ignored rule.
check warns about evidence projects that aren't in profile.yaml, keywords that
appear in two families, and exclusions that would always filter a family.
Before scoring, a local prefilter (driven by matching.yaml) filters only jobs you
can't or won't do. The stack never filters; stacks in soft_penalties only lower
the rank (−1 by default).
-
Hard exclusions:
role_typesmatched as whole words in the title, andconditions("unpaid", "service bond" ...) anywhere in the title, details or job description. Two role types have a built-in meaning:internshipalso catches "Intern" titles, Internshala internship links, listings that say "internship", and stipends ("Unpaid", up to ₹40,000 a month);non-engineeringcatches titles with no engineering word and no stack keyword.
-
Experience: each job is
verified_fresher,stretch,too_seniororunknown. Too senior is filtered. The level comes from, in order:- your quick check;
- years in the title or listing details ("(0-2 yrs)", "Experience: 1-4 yrs");
- required years in the job description, not "preferred" ones or a company's history;
- a senior word in the title ("Senior", "Lead", "SDE II", "L5" ...);
- a fresher word ("SDE 1", "fresher", "2026 batch" ...);
- a fresher-only source (Naukri Campus).
Ranges use the minimum: a minimum of up to
pass_max_min_years(1) is fresher, up tostretch_min_years(2) is a stretch, and above that is too senior. -
Location: a known place outside
location.allowedis filtered, unless the job is remote andremote_okis true.Delhi NCRcovers Delhi, Gurugram, Noida, Ghaziabad and Faridabad, and PIN codes are recognised. Jobs with no location, only "India", or only a state containing one of your cities pass.
Needs a quick check. Most alerts (all of LinkedIn's) don't say what experience a
job wants. With unknown_policy: needs_check, such jobs pass the filter but never go
into Apply today until verified. They wait in Needs a quick check, under Apply
today and as a tab on Job alerts, ranked by fit. Open the posting, read the experience
line, and click Fresher OK (eligible for Apply today) or Too senior (filtered).
Undo check reverses either.
Filtered jobs are never deleted. They stay under Filtered and All, with the
reason. mailwarden jobs prefilter prints the counts per reason and per experience
level.
A job expires when no job-alert email has shown it for expire_after_days (default 14)
days, or when its posting is closed: a Greenhouse, Lever or Ashby fetch finds it removed,
or its description says it is no longer accepting applications. Job alerts don't carry
posting dates, so "last seen in an alert" is the main signal; a job that keeps being
advertised stays current. Expired jobs move to the Expired tab. They are never
deleted, and they are no longer fetched, scored or offered in Apply today.
The LLM gives each candidate a score from 0 to 10. With it come the matched and missing skills, the project to lead with, and one sentence explaining the score.
- A preliminary score comes from the alert alone and is capped at 7.
- A full score uses the job description. A missing must-have skill caps it at 6, and experience above your level caps it at 4. If the description demands a CS/IT degree and your education isn't one, "CS degree required" is added to the missing skills.
Scores are cached, and only redone when the job or your profile changes. At most
max_scores_per_run jobs are scored per sync. Scores only order jobs; nothing is
hidden.
- ATS APIs (on by default): fetched for candidates hosted on Greenhouse, Lever or Ashby, through their public job-board APIs, and cached for 7 days.
- Job pages (on by default): with
[job_alerts] jd_fetch = "auto", each sync fetches pages for jobs that pass your filters, newest first, up tomax_jd_fetches_per_run. Set"button"to fetch only when you click Fetch job description or Fetch JDs for top 10, or"off"to never fetch job pages. Any site can be fetched exceptjd_fetch_deny(LinkedIn, Indeed, Naukri and Glassdoor by default). Each site's robots.txt is honoured, requests to the same site are spaced out, and click-tracking links are unwrapped without visiting the tracker. A page with too little readable text says "Couldn't read this page, paste JD". SECURITY.md lists every network guard. - Paste: for denied sites, opaque click-trackers and unreadable pages, the card says why. Open the job, copy the description, and use Paste JD → Save and score.
The Insights page (linked from Job alerts) shows, per site, how often job pages could be read over the last 30 days. Sites that are often "unreadable" usually build their pages with JavaScript. Automatic fetching skips a site after 5 tries in a week with none readable, so the per-run budget goes to sites that work; the button still tries it.
A job's rank is its fit score, adjusted as follows:
| Condition | Adjustment |
|---|---|
| Seen in the last 3 days | +0.5 |
| First seen more than 3 weeks ago | -1 |
Company on [job_alerts] watchlist |
+1 |
At equal rank, full scores come before preliminary ones. Apply today is a daily shortlist:
- It shows the top
apply_today_countjobs (default 8) first seen in the lastapply_today_daysdays (default 3). - No role family (Go, Java, Python, full stack/web, AI/ML, mobile, DevOps/cloud, data,
.NET/PHP) takes more than
max_family_shareof it (default 25%), unless there aren't enough other jobs. Generic titles such as "Software Engineer" or "SDE 1" are never capped. Each card shows its family. - It takes only jobs with a verified experience level (fresher, or at most
max_stretch_in_apply_todaystretch jobs). - It skips jobs you dismissed, jobs filtered out, expired jobs, and jobs whose company and role are already in your Applications.
- Applied on a card removes the job from Apply today at once, so a new job takes its place, and adds it to Applications (stage: applied). Undo applied on the Job alerts page reverses both.
You get one notification a day: "N new jobs scored 7+".
Each job records its source type and its source:
- Source types: job boards, company careers, startup platforms, communities, and company alerts sent via hiring systems.
- Sources: LinkedIn, Naukri, Naukri Campus, Internshala, Cutshort, "Bayer (jobs2web)" and so on.
The same job from several sources (same company, title and city) is shown as one card, which lists the other sources under "also on". mailwarden also tidies what alerts say:
- A title like "SAP EAM - Bangalore, IN" becomes the title "SAP EAM" with the location Bangalore.
- Relay names become real companies (Amex Careers → American Express, EYJobAlerts → EY).
.venv/bin/mailwarden jobs calibrate # label 20 jobs good/bad, then see agreement
.venv/bin/mailwarden jobs calibrate --report # re-check after profile changes and rescoringThe report shows:
- how many jobs you called good scored 7+
- how many jobs you called bad scored below 5
- the biggest disagreements, with the model's reason
Use it to adjust profile.yaml. Your labels stay in the encrypted database.
Calibration checks the scores; the recall audit checks what the ranking hides:
.venv/bin/mailwarden jobs audit # 15 jobs from outside Apply today: would you apply?
.venv/bin/mailwarden jobs audit --report # missed-good counts, reasons and suggested changesThe audit shows jobs that are not in Apply today, one at a time: about 6 filtered,
6 ranked lower, and 3 left out for other reasons (expired, outside the Apply today
window, not scored yet). Each shows the title, company, score and score type, and the
filter reason if any. Answer y (I'd apply), n, s (skip) or q. Jobs you've
audited are never shown again.
For every y, it shows exactly why the job was left out: the prefilter rule, expiry,
the window, the role-family limit, its rank against the cutoff (including ties), the
title-only score, a score cap, or low skill weights. Each reason comes with the change
that would have surfaced the job, such as "profile.yaml: add 'Pune' to locations" or
"config: raise [job_alerts] apply_today_days (now 3)".
The report shows audited and missed-good counts for the last 7 days and all time, the reasons grouped by how many missed jobs they explain, and the suggested changes. It changes nothing by itself. Answers are stored in the encrypted database, and Apply today reminds you when a week has passed since the last audit.
sender_rules.yaml in the mailwarden home has four tiers. A domain rule also matches
its subdomains, and an exact address beats a domain rule.
- priority: job mail. Application-tracking and assessment platforms are pre-seeded. Priority mail still goes through the gate.
- sensitive: banks, UPI and payment apps, cards, brokers, tax and government, and account-security senders. This mail is never sent to any LLM.
- ignore: senders you never want processed. They are only counted.
- job_alert: job-board alerts (LinkedIn job alerts, Indeed, Naukri, foundit, Internshala). They are labelled by rule and never notify. Their jobs are extracted for Job alerts.
Independently of tiers, the gate inspects the sender name, subject and body. It holds back anything that looks like an OTP or verification code, a password reset, a login alert, a bank, UPI or card transaction, a statement, or KYC, PAN, Aadhaar or tax mail. Promoting a sender never bypasses these content checks. Two exceptions keep job mail visible:
- A recruiting subdomain of a sensitive company (e.g.
recruitment.americanexpress.com) counts as priority. - Job mail that was held back only because of footer wording ("do not share", "password") is released when it clearly comes from a hiring system.
SECURITY.md has the exact rules. To tune them:
.venv/bin/mailwarden dry-run --last 100 --summary # what was held back, and why
.venv/bin/mailwarden promote talent@somefintech.com priority
.venv/bin/mailwarden promote offers.somestore.com ignore
.venv/bin/mailwarden regate --days 7 # re-check stored mail with today's rules
.venv/bin/mailwarden regate --days 7 --apply # reprocess what changedJob mail that needs action triggers a notification: an assessment, interview or offer, or an action for a company you're tracking or a priority sender. The notification shows only the company, the stage and the deadline. For held-back job mail it says "needs your attention". Clicking it opens that entry.
- macOS: after
mailwarden launcher, the mailwarden app posts notifications itself, with its name and logo, and a click opens the entry in the app window. Without the app, mailwarden usesterminal-notifierif it is installed, otherwiseosascript. macOS attributesosascriptnotifications to Script Editor, so a click opens Script Editor.mailwarden notify-testsends a test notification. - Linux and Windows: notifications use
desktop-notifier. Clicks are handled only while the sending process is alive ([notifications] click_wait_seconds).
There is at most one digest notification per day.
config.toml in the mailwarden home holds non-secret settings only. Secrets are in
the keyring. These are the settings you're most likely to change:
| Setting | Default | Meaning |
|---|---|---|
[llm] backend |
groq |
groq or ollama |
[llm] max_body_chars |
1500 | Body characters sent per email, after redaction |
[llm] max_llm_calls_per_minute |
6 | Local pacing for Groq's free tier |
[llm] max_llm_tokens_per_minute |
7000 | Local pacing; Groq's free tier allows 8,000 tokens a minute |
[llm] max_llm_calls_per_run |
150 | Cap per sync; unfinished work stays pending |
[groq] model |
openai/gpt-oss-20b |
Any Groq model with strict structured outputs |
[gmail] full_sync_days |
7 | Days fetched on the first run |
[dashboard] port |
8765 | Local dashboard port (the host is always 127.0.0.1) |
[notifications] enabled |
true | Desktop notifications |
[digest] times |
08:00, 18:00 | When the digest is built |
[digest] markdown_dir |
off | Optional plaintext Markdown copy of the digest |
[job_alerts] watchlist |
[] |
Companies whose jobs get +1 rank |
[job_alerts] apply_today_count |
8 | Jobs in Apply today |
[job_alerts] apply_today_min_score |
0 (off) | Weaker matches never take an Apply today slot (still in Browse); useful once most jobs have full scores |
[job_alerts] tier_a_count / tier_b_count |
5 / 10 | Today's Tier A (full score 7+) and Tier B sizes |
[job_alerts] apply_today_days |
3 | Apply today picks from jobs first seen in this many days (0 = any age) |
[job_alerts] max_family_share |
0.25 | Largest share of Apply today from one role family (0 = no cap) |
[job_alerts] expire_after_days |
14 | Days without an alert before a job expires (0 = only closed postings expire) |
[job_alerts] jd_auto_fetch |
true | Automatic fetches from Greenhouse, Lever and Ashby APIs |
[job_alerts] jd_fetch |
"auto" | Job pages on other sites: "auto", "button" or "off" |
[job_alerts] jd_fetch_deny |
LinkedIn, Indeed, Naukri, Glassdoor | Sites never fetched (paste instead) |
[job_alerts] jd_fetch_interval_seconds |
5 | Gap between requests to the same site |
[job_alerts] max_scores_per_run |
40 | Jobs scored per sync |
[job_alerts] max_scores_per_day |
80 | Jobs scored per day, so the free LLM quota stays available for your email |
[job_alerts] max_jd_fetches_per_run |
30 | Job descriptions fetched per sync |
[job_alerts] daily_notification |
true | "N new jobs scored 7+", once a day |
| Command | What it does |
|---|---|
mailwarden init [--reset-rules] |
Create the home folder and default config. --reset-rules restores the seeded sender_rules.yaml and keeps the old file as .bak |
mailwarden add-account --provider gmail --name <n> [--client-secrets FILE] |
Google sign-in; the token is stored in the keyring |
mailwarden accounts |
List accounts (addresses masked) |
mailwarden reauth <n> |
Sign an existing account in again after its sign-in expired, keeping all its data (must be the same Gmail address) |
mailwarden forget-account <n> |
Revoke the token at Google, delete it, and delete the account's stored data |
mailwarden set-groq-key |
Store the Groq API key in the keyring (hidden input) |
mailwarden doctor [--sync] [--llm] |
Check the keyring, permissions, allowlist, scopes and notifications. --sync counts recent messages; --llm tests the classifier with a fake email |
mailwarden dry-run [--last N] [--account n] [--summary] [--with-llm] |
Preview tiers, gate decisions and redacted LLM text; stores nothing |
mailwarden run |
One sync: classify mail, extract and score jobs, notify |
mailwarden digest |
Build the digest now, and send the daily jobs notification |
mailwarden regate [--days 7] [--apply] [--message ID] |
Re-check stored mail with the current rules (no LLM). --apply reprocesses what changed |
mailwarden promote <address-or-domain> <tier> |
Move a sender to priority, sensitive, ignore, job_alert or default |
mailwarden matching init / matching check |
Create the matching policy from the template / validate it |
mailwarden profile init |
Create the profile folder |
mailwarden profile build [--dry-run] |
Build profile.yaml from your CV and projects |
mailwarden jobs prefilter [--show summary|filtered|kept|all] |
Show which jobs the prefilter keeps or filters, and why |
mailwarden jobs fetch [--limit N] |
Fetch job descriptions from the Greenhouse, Lever and Ashby APIs now |
mailwarden jobs score [--limit N] |
Score jobs now |
mailwarden jobs calibrate [--count 20] [--report] |
Label jobs good or bad, and see how well the scores agree |
mailwarden jobs audit [--count 15] [--report] |
Weekly recall audit: would you apply to jobs Apply today left out, and why were they left out |
mailwarden dashboard [--open] |
Run the dashboard in the foreground and print a one-time sign-in link |
mailwarden open [--print-only] |
Open a one-time sign-in link to the running dashboard |
mailwarden app [--path /jobs] |
Open the dashboard in its own window (macOS) |
mailwarden launcher [--browser] |
Create ~/Applications/mailwarden.app (macOS). --browser makes it open your browser instead |
mailwarden notify-test |
Send a test notification |
mailwarden schedule [--platform macos|linux|windows] |
Generate background-job files and print install commands |
Put --quiet before any command for warnings-only output (scheduled jobs use it), or
-v for debug logs.
- Keychain asks for your password for "mailwarden-python" (macOS). The app runs its own copy of Python, so macOS asks once for each stored secret. Enter your Mac login password and choose Always Allow.
- The window stays white on launch. The app is waiting for a Keychain prompt to be answered.
- Clicking a notification opens Script Editor. The app isn't installed. Run
mailwarden launcher.mailwarden doctorshows which notifier is in use. - "No new mail from : Google's sign-in expired" (or "Google rejected the
stored sign-in" in the logs). The refresh token expired (the consent screen is in
Testing) or was revoked. Run
mailwarden reauth <account>; it keeps all the account's data. See "Seven-day sign-ins" above. - The digest or Apply today looks empty. Check for the banner above: while mail isn't syncing, the digest page shows the last digest that had mail, and Apply today keeps the jobs from the last alerts that arrived.
- Scoring is slow or stops at a rate limit. Groq's free tier allows about three scorings a minute. Unfinished jobs are picked up on the next sync.
make test # pytest, including the mandatory zero-LLM-calls test for sensitive mail
make lock # regenerate the hash-pinned lockfiles after changing pyproject.toml
make audit # pip-audit on the lockfile, plus bandit on the codescripts/make_icon.sh rebuilds the app icon from mailwarden/assets/logo-source.png.