rscontacts - Google Contacts CLI Tool
A command-line tool for managing and auditing Google Contacts, written in Rust.
Features
- List and inspect contacts with flexible display options
- Audit contacts with 16+ automated checks for data quality issues
- Fix issues interactively with
--fixsupport on most checks - Dry-run mode to preview changes before applying them
- Stats mode for a quick summary of all issues
- Shell completions for bash, zsh, fish, and more
Checks Available
rscontacts can detect and fix:
- Non-English names, all-caps names, names not starting with a capital letter
- Reversed name order (e.g., “Veltzer, Mark” instead of “Mark Veltzer”)
- Phone numbers missing country codes or not in
+CC-NUMBERformat - Phone numbers without labels (mobile/home/work)
- Non-English phone labels
- Invalid email addresses and emails with uppercase letters
- Duplicate phone numbers and emails on the same contact
- Contacts not assigned to any contact group (label)
- Empty contact groups (labels with no members)
- Contact group names containing spaces
Technology
- Built with Rust using the google-people1 crate
- OAuth2 authentication via yup-oauth2
- CLI powered by clap
Installation
Prerequisites
- Rust toolchain (edition 2024)
- Google Cloud project with People API enabled
- OAuth2 credentials (Desktop application type)
Install from crates.io
cargo install rscontacts
This downloads, compiles, and installs the latest published version into ~/.cargo/bin/.
Building from Source
git clone https://github.com/veltzer/rscontacts.git
cd rscontacts
cargo build --release
The binary will be at target/release/rscontacts.
Google Cloud Setup
- Go to Google Cloud Console
- Create a new project (or use an existing one)
- Enable the People API under APIs & Services
- Create OAuth2 credentials:
- Go to APIs & Services > Credentials
- Click “Create Credentials” > “OAuth client ID”
- Choose “Desktop application” as the application type
- Download the JSON file
- Place the credentials file at
~/.config/rscontacts/credentials.json
Getting Started
First-Time Setup
After installing rscontacts and placing your OAuth2 credentials, authenticate:
rscontacts auth
This opens your browser for Google OAuth2 consent. The token is cached at ~/.config/rscontacts/token_cache.json for future use.
If you’re on a headless machine:
rscontacts auth --no-browser
This prints the auth URL instead of opening a browser.
Basic Usage
List all contacts:
rscontacts list
Include email addresses:
rscontacts list --emails
Show phone labels (mobile/home/work):
rscontacts list --labels
Running Checks
Run all checks at once:
rscontacts check-all
Get a summary of issues:
rscontacts check-all --stats
Fix issues interactively:
rscontacts check-all --fix
Preview what would change without modifying anything:
rscontacts check-all --fix --dry-run
Inspecting a Contact
Show all details for a specific contact:
rscontacts show-contact "John"
This does a case-insensitive substring search and displays all available fields.
Authentication
rscontacts uses OAuth2 to access the Google People API on your behalf.
How It Works
- You provide OAuth2 client credentials (a JSON file from Google Cloud Console)
- On first use, rscontacts opens your browser to get consent
- The access token is cached locally for future requests
Files
| File | Location | Purpose |
|---|---|---|
| Credentials | ~/.config/rscontacts/credentials.json | OAuth2 client ID and secret |
| Token cache | ~/.config/rscontacts/token_cache.json | Cached access/refresh tokens |
Commands
Authenticate (opens browser):
rscontacts auth
Authenticate without browser (prints URL):
rscontacts auth --no-browser
Force re-authentication (removes cached token first):
rscontacts auth --force
Scopes
rscontacts requests the https://www.googleapis.com/auth/contacts scope, which provides full read/write access to your Google Contacts.
Commands
rscontacts provides commands in several categories. Most check commands support --fix and --dry-run flags.
Setup & Diagnostics
- auth — Authenticate with Google (opens browser for OAuth2 consent)
- init-config — Generate a default config file at ~/.config/rscontacts/config.toml
- test-connect — Test connectivity to the Google People API
- complete — Generate shell completions
- version — Print version information
Listing & Inspection
- list — List all contacts
- show-contact — Show all details about a specific contact
- edit-contact — Interactively edit a contact
- show-contact-labels — Show all contact labels (contact groups) in use
- show-email-labels — Show all distinct email labels in use
- show-phone-labels — Show all distinct phone labels in use
- export-json — Export all contacts as JSON
Run All Checks
- all-checks — Run all checks
Name Checks
- check-contact-given-name-regexp — Check given names against allow regex defined in config.toml
- check-contact-family-name-regexp — Check family names against allow regex defined in config.toml
- check-contact-suffix-regexp — Check suffixes against allow regex (default: numeric)
- check-contact-displayname-duplicate — Print contacts that share the same display name
- check-contact-no-displayname — Print contacts with an empty display name
- check-contact-no-given-name — Check contacts that have no given name but have a family name
- check-contact-no-middle-name — Check that no contact has a middle name set
- check-contact-no-nickname — Check that no contact has a nickname set
- check-contact-given-name-known — Check that all given names are in the allowed list from config
- check-contact-given-name-exists — Check that every given name in the config has at least one contact
Company Checks
- check-contact-company-exists — Check that all company fields are in the known companies list from config
- check-contact-company-known — Check that every company in the config has at least one contact
- check-contact-type — Check that every contact has exactly one of type:Person or type:Company labels
- check-contact-no-identity — Check contacts that have no type tag (type:Person or type:Company)
- check-contact-type-company-given-name — Check that contacts tagged type:Company have given name equal to company field
- check-contact-type-company-no-company — Check that contacts tagged type:Company have their company field set
- check-contact-type-company-no-label — Check that contacts tagged type:Company have a matching company:<name> label
Phone Checks
- check-phone-countrycode — Print contacts with phone numbers missing a country code
- check-phone-format — Print phone numbers not in +CC-NUMBER format
- check-phone-duplicate — Print contacts that have the same phone number attached twice
- check-phone-label-missing — Print contacts with phone numbers missing a label (mobile/home/work/etc)
- check-phone-label-english — Print contacts with non-English phone labels
- check-phone-country-label — Check that contacts have correct country:<Name> labels matching their phone country codes
Email Checks
- check-contact-email — Print contacts with invalid or uppercase email addresses
- check-contact-email-duplicate — Print contacts that have the same email address attached twice
Contact Group (Label) Checks
- check-contact-no-label — Print contacts not assigned to any label (contact group)
- check-contact-label-nophone — Print labels (contact groups) that have no contacts
- check-contact-label-regexp — Check contact labels (groups) against allow regex defined in config.toml
Merge Commands
- merge-by-phone — Find and merge contacts that share the same phone number
- merge-by-email — Find and merge contacts that share the same email address
Action Commands
- move-family-to-suffix — Move numeric family names to suffix for contacts that have no suffix
- move-suffix-to-family — Move numeric suffixes to family name for contacts that have no family name
- compact-suffixes-for-contacts — Compact suffixes for contacts sharing the same base name (given + family)
- remove-label-from-all-contacts — Remove a contact label (group) from all contacts that have it
- review-phone-label — Review all phones with a specific label (e.g. “Work Fax”)
- review-email-label — Review all emails with a specific label (e.g. “Work”)
- sync-gnome-contacts — Sync Google Contacts to GNOME Contacts (Evolution Data Server)
auth
Authenticate with Google via OAuth2.
Usage
rscontacts auth
rscontacts auth --no-browser
rscontacts auth --force
Flags
| Flag | Description |
|---|---|
--no-browser | Print the auth URL instead of opening a browser |
--force | Delete cached token and re-authenticate |
See Authentication for more details.
init-config
Generate a default config file at ~/.config/rscontacts/config.toml.
Usage
rscontacts init-config
rscontacts init-config --force
Flags
| Flag | Description |
|---|---|
--force | Overwrite the config file if it already exists |
test-connect
Test connectivity to the Google People API.
Usage
rscontacts test-connect
Notes
Makes a simple API call to verify that authentication is working and the Google People API is reachable.
complete
Generate shell completion scripts.
Usage
rscontacts complete bash > ~/.local/share/bash-completion/completions/rscontacts
rscontacts complete zsh > ~/.zfunc/_rscontacts
rscontacts complete fish > ~/.config/fish/completions/rscontacts.fish
Supported Shells
- bash
- zsh
- fish
- elvish
- powershell
version
Print version and build information.
Usage
rscontacts version
Output
rscontacts 0.1.0 by Mark Veltzer <mark.veltzer@gmail.com>
GIT_DESCRIBE: v0.1.0
GIT_SHA: abc1234
GIT_BRANCH: master
GIT_DIRTY: false
RUSTC_SEMVER: 1.85.0
RUST_EDITION: 2024
list
List all contacts with their phone numbers.
Usage
rscontacts list
rscontacts list --emails
rscontacts list --labels
rscontacts list --emails --labels
Flags
| Flag | Description |
|---|---|
--emails | Also show email addresses |
--labels | Also show contact labels (contact group memberships) |
--starred | Only show starred contacts |
Output Format
Default (name and first phone number):
Mark Veltzer | +972-505665636
John Doe
With --emails:
Mark Veltzer | mark@example.com | +972-505665636
With --labels:
Mark Veltzer | +972-505665636 | [Friends, Work]
John Doe | +972-501234567 | [Family]
show-contact
Show all available details about a specific contact.
Usage
rscontacts show-contact "John"
rscontacts show-contact "Doe"
How It Works
Performs a case-insensitive substring search on contact names. If multiple contacts match, all are displayed separated by a divider.
Fields Displayed
- Name (given, family, middle, prefix, suffix)
- Nicknames
- Email addresses (with type)
- Phone numbers (with type)
- Addresses
- Organizations (title, company, department)
- Birthdays
- Relations
- Events
- Biographies
- URLs
- IM clients
- SIP addresses
- Occupations, interests, skills
- Locations
- External IDs
- Custom fields and client data
- Contact group memberships (labels)
- Resource name (internal Google ID)
edit-contact
Interactively edit a contact.
Usage
rscontacts edit-contact "John Doe"
rscontacts edit-contact "John"
Arguments
| Argument | Description |
|---|---|
name | Name (or partial name) of the contact to edit |
Notes
Performs a case-insensitive substring search on contact names. If multiple contacts match, you will be prompted to select one.
show-contact-labels
Show all contact groups (labels) with their member counts.
Usage
rscontacts show-contact-labels
Output
Friends (12)
Family (5)
Work (23)
show-email-labels
Show all distinct email labels in use.
Usage
rscontacts show-email-labels
Notes
Lists every unique email label (type) found across all contacts.
show-phone-labels
Show all distinct phone labels (types) currently in use across all contacts.
Usage
rscontacts show-phone-labels
Output
Lists each unique label alphabetically:
home
mobile
work
export-json
Export all contacts as JSON.
Usage
rscontacts export-json
rscontacts export-json --short
Flags
| Flag | Description |
|---|---|
--short | Export a shorter/summarized version of each contact |
all-checks
Run all checks at once.
Usage
rscontacts all-checks
rscontacts all-checks --fix
rscontacts all-checks --fix --dry-run
rscontacts all-checks --stats
rscontacts all-checks --verbose
rscontacts all-checks --country 1
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix all issues found |
--dry-run | Show what would change without modifying anything |
--stats | Only show error counts per check, no details |
--verbose, -v | Show verbose output |
--country <CODE> | Country code for phone formatting (default: 972) |
Notes
All individual check commands are run in sequence. Each section header includes the corresponding command name so you know which standalone command to use for fixing specific issues.
With --fix, all checks support interactive fixing – the fix/dry-run flags are passed through to every check.
check-contact-given-name-regexp
Check given names against the allow regex defined in config.toml.
Usage
rscontacts check-contact-given-name-regexp
rscontacts check-contact-given-name-regexp --fix
rscontacts check-contact-given-name-regexp --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix non-matching given names |
--dry-run | Show what would change without modifying anything |
Notes
The allow regex is configured in the [check-contact-given-name-regexp] section of config.toml.
check-contact-family-name-regexp
Check family names against the allow regex defined in config.toml.
Usage
rscontacts check-contact-family-name-regexp
rscontacts check-contact-family-name-regexp --fix
rscontacts check-contact-family-name-regexp --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix non-matching family names |
--dry-run | Show what would change without modifying anything |
Notes
The allow regex is configured in the [check-contact-family-name-regexp] section of config.toml. Pure numeric family names are also allowed by default.
check-contact-suffix-regexp
Check suffixes against the allow regex (default: numeric only).
Usage
rscontacts check-contact-suffix-regexp
rscontacts check-contact-suffix-regexp --fix
rscontacts check-contact-suffix-regexp --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix non-matching suffixes |
--dry-run | Show what would change without modifying anything |
Notes
The allow regex is configured in the [check-contact-suffix-regexp] section of config.toml. By default, only numeric suffixes are allowed.
check-contact-displayname-duplicate
Print contacts that share the same display name.
Usage
rscontacts check-contact-displayname-duplicate
rscontacts check-contact-displayname-duplicate --fix
rscontacts check-contact-displayname-duplicate --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix duplicate display names |
--dry-run | Show what would change without modifying anything |
check-contact-no-displayname
Print contacts with an empty display name.
Usage
rscontacts check-contact-no-displayname
rscontacts check-contact-no-displayname --fix
rscontacts check-contact-no-displayname --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix contacts with empty display names |
--dry-run | Show what would change without modifying anything |
check-contact-no-given-name
Check contacts that have no given name but have a family name.
Usage
rscontacts check-contact-no-given-name
rscontacts check-contact-no-given-name --fix
rscontacts check-contact-no-given-name --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix contacts missing a given name |
--dry-run | Show what would change without modifying anything |
check-contact-no-middle-name
Check that no contact has a middle name set.
Usage
rscontacts check-contact-no-middle-name
rscontacts check-contact-no-middle-name --fix
rscontacts check-contact-no-middle-name --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix contacts with middle names |
--dry-run | Show what would change without modifying anything |
check-contact-no-nickname
Check that no contact has a nickname set.
Usage
rscontacts check-contact-no-nickname
rscontacts check-contact-no-nickname --fix
rscontacts check-contact-no-nickname --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix contacts with nicknames |
--dry-run | Show what would change without modifying anything |
check-contact-given-name-known
Check that all given names are in the allowed list from config.
Usage
rscontacts check-contact-given-name-known
rscontacts check-contact-given-name-known --fix
rscontacts check-contact-given-name-known --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix unknown given names |
--dry-run | Show what would change without modifying anything |
Notes
The allowed names list is configured in the [check-contact-given-name-known] section of config.toml via the names array.
check-contact-given-name-exists
Check that every given name in the config has at least one contact.
Usage
rscontacts check-contact-given-name-exists
rscontacts check-contact-given-name-exists --fix
rscontacts check-contact-given-name-exists --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix given names with no contacts |
--dry-run | Show what would change without modifying anything |
check-contact-company-exists
Check that all company fields are in the known companies list from config.
Usage
rscontacts check-contact-company-exists
rscontacts check-contact-company-exists --fix
rscontacts check-contact-company-exists --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix contacts with unknown companies |
--dry-run | Show what would change without modifying anything |
check-contact-company-known
Check that every company in the config has at least one contact.
Usage
rscontacts check-contact-company-known
rscontacts check-contact-company-known --fix
rscontacts check-contact-company-known --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix companies with no contacts |
--dry-run | Show what would change without modifying anything |
check-contact-type
Check that every contact has exactly one of type:Person or type:Company labels.
Usage
rscontacts check-contact-type
rscontacts check-contact-type --fix
rscontacts check-contact-type --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix contacts with missing or duplicate type labels |
--dry-run | Show what would change without modifying anything |
check-contact-no-identity
Check contacts that have no type tag (type:Person or type:Company).
Usage
rscontacts check-contact-no-identity
rscontacts check-contact-no-identity --fix
rscontacts check-contact-no-identity --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively assign a type tag to untagged contacts |
--dry-run | Show what would change without modifying anything |
check-contact-type-company-given-name
Check that contacts tagged type:Company have given name equal to company field.
Usage
rscontacts check-contact-type-company-given-name
rscontacts check-contact-type-company-given-name --fix
rscontacts check-contact-type-company-given-name --auto-fix
rscontacts check-contact-type-company-given-name --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix mismatched given names |
--auto-fix | Automatically set given name to match company field |
--dry-run | Show what would change without modifying anything |
check-contact-type-company-no-company
Check that contacts tagged type:Company have their company field set.
Usage
rscontacts check-contact-type-company-no-company
rscontacts check-contact-type-company-no-company --fix
rscontacts check-contact-type-company-no-company --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix company contacts missing the company field |
--dry-run | Show what would change without modifying anything |
check-contact-type-company-no-label
Check that contacts tagged type:Company have a matching company:<name> label.
Usage
rscontacts check-contact-type-company-no-label
rscontacts check-contact-type-company-no-label --fix
rscontacts check-contact-type-company-no-label --auto-fix
rscontacts check-contact-type-company-no-label --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix missing company labels |
--auto-fix | Automatically create and assign the company label |
--dry-run | Show what would change without modifying anything |
check-phone-countrycode
Find phone numbers missing a country code.
Usage
rscontacts check-phone-countrycode
rscontacts check-phone-countrycode --fix
rscontacts check-phone-countrycode --fix --country 1
rscontacts check-phone-countrycode --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Add country code to matching phone numbers |
--dry-run | Show changes without applying |
--country <CODE> | Country code to prepend (default: 972) |
Fix Behavior
Automatically prepends +<country> to phone numbers that don’t start with + or 00. Leading zeros are stripped (e.g., 0505665636 becomes +972505665636).
check-phone-format
Find phone numbers not in the standard +CC-NUMBER format.
The expected format is: +<country code>-<digits> with exactly one dash separating the country code from the number, and no other separators. Examples:
+972-505665636(Israel)+1-5551234567(US)+7-9268335991(Russia)+44-2079460958(UK)
Usage
rscontacts check-phone-format
rscontacts check-phone-format --fix
rscontacts check-phone-format --fix --country 972
rscontacts check-phone-format --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Reformat phone numbers to +CC-NUMBER |
--dry-run | Show changes without applying |
--country <CODE> | Default country code for numbers without one (default: 972) |
Country Code Detection
When fixing, rscontacts uses a built-in table of ITU country codes to correctly detect the country code length. For example, +79268335991 is correctly split as +7-9268335991 (Russia, 1-digit code) rather than +792-68335991.
What Gets Fixed
- Missing dash:
+972505665636→+972-505665636 - Extra dashes:
+972-50-5665636→+972-505665636 - Spaces:
+972 50 566 5636→+972-505665636 00prefix:00972505665636→+972-505665636- No country code:
0505665636→+972-505665636(using--country)
Non-numeric phone entries (e.g., “VIVINO RLZ”) are skipped.
check-phone-duplicate
Print contacts that have the same phone number attached twice.
Usage
rscontacts check-phone-duplicate
rscontacts check-phone-duplicate --fix
rscontacts check-phone-duplicate --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix duplicate phone entries |
--dry-run | Show what would change without modifying anything |
check-phone-label-missing
Print contacts with phone numbers missing a label (mobile/home/work/etc).
Usage
rscontacts check-phone-label-missing
rscontacts check-phone-label-missing --fix
rscontacts check-phone-label-missing --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively assign labels to unlabeled phone numbers |
--dry-run | Show what would change without modifying anything |
Notes
Phone “labels” refer to the type field on a phone number (e.g., mobile, home, work). This is distinct from contact labels (contact groups).
check-phone-label-english
Find phone numbers whose type label contains non-English (non-ASCII) characters, such as “Мобильный” (Russian) or “נייד” (Hebrew) instead of “mobile”.
Usage
rscontacts check-phone-label-english
rscontacts check-phone-label-english --fix
rscontacts check-phone-label-english --fix --dry-run
Output
Alex | +972542518077 [Мобильный]
Oren | +972528478018 [נייד]
Fix Behavior
With --fix, prompts with predefined English label choices:
Label for Alex's phone? [m]obile/[h]ome/[w]ork/m[a]in/[o]ther/[s]kip:
check-phone-country-label
Ensure every contact has the correct country:<Name> labels matching their phone number country codes — and no stale country labels for countries where they have no phone numbers.
Usage
rscontacts check-phone-country-label
rscontacts check-phone-country-label --fix
rscontacts check-phone-country-label --fix --dry-run
What It Checks
Two things:
- Missing country labels: If a contact has a phone number with country code +972 (Israel), they should have a
country:Israellabel. If the label is missing, it is flagged. - Stale country labels: If a contact has a
country:Russialabel but none of their phone numbers have a +7 country code, the label is flagged.
Fix Behavior
With --fix:
- Missing labels: The
country:<Name>label is created (if it doesn’t exist) and automatically assigned to the contact. - Stale labels: The
country:<Name>label is automatically removed from the contact.
With --fix --dry-run, shows what would be changed without modifying anything.
Supported Countries
All ITU-T E.164 country codes are recognized, including:
| Code | Country | Code | Country |
|---|---|---|---|
| +1 | USA | +44 | UK |
| +7 | Russia | +49 | Germany |
| +33 | France | +86 | China |
| +34 | Spain | +91 | India |
| +39 | Italy | +380 | Ukraine |
| +41 | Switzerland | +972 | Israel |
| +971 | UAE | +966 | Saudi Arabia |
And many more — see the full mapping in src/helpers.rs.
Notes
- Only phone numbers with a recognized country code prefix are considered. Phones without country codes are ignored (fix those first with check-phone-countrycode).
- Only
country:labels that match a recognized country name are considered for removal. Custom labels that happen to start withcountry:but use a different name will not be touched. - This check is included in check-all.
check-contact-email
Print contacts with invalid or uppercase email addresses.
Usage
rscontacts check-contact-email
rscontacts check-contact-email --fix
rscontacts check-contact-email --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix invalid or uppercase emails |
--dry-run | Show what would change without modifying anything |
check-contact-email-duplicate
Print contacts that have the same email address attached twice.
Usage
rscontacts check-contact-email-duplicate
rscontacts check-contact-email-duplicate --fix
rscontacts check-contact-email-duplicate --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix duplicate email entries |
--dry-run | Show what would change without modifying anything |
check-contact-no-label
Find contacts that are not assigned to any contact group (label).
Usage
rscontacts check-contact-no-label
rscontacts check-contact-no-label --fix
rscontacts check-contact-no-label --fix --dry-run
Fix Behavior
With --fix, shows full contact details (name, phones, emails, organization, etc.) and prompts for each unlabeled contact:
[l]abel / [d]elete / [s]kip:
- label: Shows contact details again, then prompts with tab-completion for existing labels. You can also type a new label name — if it doesn’t exist, you’ll be asked to create it.
- delete: Asks for confirmation before deleting the contact.
- skip: Moves on to the next contact.
Notes
In Google Contacts, “labels” are contact groups (e.g., “Friends”, “Family”, “Work”). This check finds contacts that have no group membership (excluding the default “myContacts” system group).
This is different from check-phone-no-label, which checks phone number type labels.
check-contact-label-nophone
Print labels (contact groups) that have no contacts with phone numbers.
Usage
rscontacts check-contact-label-nophone
rscontacts check-contact-label-nophone --fix
rscontacts check-contact-label-nophone --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix empty labels |
--dry-run | Show what would change without modifying anything |
check-contact-label-regexp
Check contact labels (groups) against the allow regex defined in config.toml.
Usage
rscontacts check-contact-label-regexp
rscontacts check-contact-label-regexp --fix
rscontacts check-contact-label-regexp --fix --dry-run
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix non-matching labels |
--dry-run | Show what would change without modifying anything |
merge-by-phone
Find and merge contacts that share the same phone number across different contacts.
Usage
rscontacts merge-by-phone
rscontacts merge-by-phone --fix
rscontacts merge-by-phone --fix --dry-run
What It Does
Scans all contacts and identifies groups of contacts that share one or more phone numbers. Phone numbers are normalized (digits only, stripping international prefix 00) before comparison, so +972-501234567 and 00972501234567 are treated as the same number.
Contacts are grouped using connected components: if contact A shares a phone with contact B, and contact B shares a different phone with contact C, all three are in the same merge group.
Without --fix, displays each group showing:
- All contacts in the group with full details
- Which phone numbers are shared
Fix Behavior
With --fix, for each group of N contacts you are prompted with:
[d1]elete [e1]dit [d2]elete [e2]dit ... [m]erge / [s]kip
- d1, d2, … — Delete contact 1, 2, etc. (asks for confirmation). The contact is removed from the group and the prompt re-displays with remaining contacts.
- e1, e2, … — Edit contact 1, 2, etc. using the interactive editor (same as
edit-contact). Useful for cleaning up a contact before merging. - m (merge) — Pick which contact to keep. All fields from the other contacts are merged into it, then the others are deleted.
- n (next) — Move on to the next group.
If you delete contacts until only one remains, the group is resolved automatically.
Merge Details
When merging, the command:
- Merges phone numbers — adds any phones from source contacts not already on the target (compared by normalized digits)
- Merges email addresses — adds any emails from source contacts not already on the target (compared case-insensitively)
- Merges addresses — adds any addresses not already on the target (compared by formatted value)
- Merges organization — copies from source only if the target has no organization
- Merges birthdays — copies from source only if the target has no birthday
- Merges biographies — copies from source only if the target has no biography
- Copies labels — adds all contact group memberships from source contacts to the target
- Deletes source contacts — removes the merged-away contacts
With --fix --dry-run, shows what would happen without making changes.
Notes
- Only “fixable” phone numbers are considered (see
is_fixable_phone— star codes, short codes, and alphanumeric entries are skipped). - The merge is additive for multi-value fields (phones, emails, addresses, labels) and first-wins for single-value fields (organization, birthday, biography).
- The target contact is re-fetched before updating to ensure a fresh etag, avoiding conflicts.
- This is NOT included in
check-allsince it is a destructive operation requiring careful interactive review.
merge-by-email
Find and merge contacts that share the same email address across different contacts.
Usage
rscontacts merge-by-email
rscontacts merge-by-email --fix
rscontacts merge-by-email --fix --dry-run
What It Does
Scans all contacts and identifies groups of contacts that share one or more email addresses. Emails are compared case-insensitively, so John@Example.com and john@example.com are treated as the same address.
Contacts are grouped using connected components: if contact A shares an email with contact B, and contact B shares a different email with contact C, all three are in the same merge group.
Without --fix, displays each group showing:
- All contacts in the group with full details
- Which email addresses are shared
Fix Behavior
With --fix, for each group of N contacts you are prompted with:
[d1]elete [e1]dit [d2]elete [e2]dit ... [m]erge / [n]ext
- d1, d2, … — Delete contact 1, 2, etc. (asks for confirmation). The contact is removed from the group and the prompt re-displays with remaining contacts.
- e1, e2, … — Edit contact 1, 2, etc. using the interactive editor (same as
edit-contact). Useful for cleaning up a contact before merging. - m (merge) — Pick which contact to keep. All fields from the other contacts are merged into it, then the others are deleted.
- n (next) — Move on to the next group.
If you delete contacts until only one remains, the group is resolved automatically.
Merge Details
When merging, the command:
- Merges phone numbers — adds any phones from source contacts not already on the target (compared by normalized digits)
- Merges email addresses — adds any emails from source contacts not already on the target (compared case-insensitively)
- Merges addresses — adds any addresses not already on the target (compared by formatted value)
- Merges organization — copies from source only if the target has no organization
- Merges birthdays — copies from source only if the target has no birthday
- Merges biographies — copies from source only if the target has no biography
- Copies labels — adds all contact group memberships from source contacts to the target
- Deletes source contacts — removes the merged-away contacts
With --fix --dry-run, shows what would happen without making changes.
Notes
- The merge is additive for multi-value fields (phones, emails, addresses, labels) and first-wins for single-value fields (organization, birthday, biography).
- The target contact is re-fetched before updating to ensure a fresh etag, avoiding conflicts.
- This is NOT included in
check-allsince it is a destructive operation requiring careful interactive review.
move-family-to-suffix
Move numeric family names to suffix for contacts that have no suffix.
Usage
rscontacts move-family-to-suffix
rscontacts move-family-to-suffix --dry-run
Flags
| Flag | Description |
|---|---|
--dry-run | Show what would change without modifying anything |
move-suffix-to-family
Move numeric suffixes to family name for contacts that have no family name.
Usage
rscontacts move-suffix-to-family
rscontacts move-suffix-to-family --dry-run
Flags
| Flag | Description |
|---|---|
--dry-run | Show what would change without modifying anything |
compact-suffixes-for-contacts
Compact suffixes for contacts sharing the same base name (given + family).
Usage
rscontacts compact-suffixes-for-contacts
rscontacts compact-suffixes-for-contacts --dry-run
Flags
| Flag | Description |
|---|---|
--dry-run | Show what would change without modifying anything |
remove-label-from-all-contacts
Remove a contact label (group) from all contacts that have it.
Usage
rscontacts remove-label-from-all-contacts "Old Label"
rscontacts remove-label-from-all-contacts "Old Label" --dry-run
Arguments
| Argument | Description |
|---|---|
label | The label (contact group) name to remove |
Flags
| Flag | Description |
|---|---|
--dry-run | Show what would change without modifying anything |
review-phone-label
Review all phones with a specific label (e.g. “Work Fax”).
Usage
rscontacts review-phone-label "Work Fax"
rscontacts review-phone-label "Work Fax" --fix
rscontacts review-phone-label "Work Fax" --fix --dry-run
Arguments
| Argument | Description |
|---|---|
label | The phone label to review |
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix phone labels |
--dry-run | Show what would change without modifying anything |
review-email-label
Review all emails with a specific label (e.g. “Work”).
Usage
rscontacts review-email-label "Work"
rscontacts review-email-label "Work" --fix
rscontacts review-email-label "Work" --fix --dry-run
Arguments
| Argument | Description |
|---|---|
label | The email label to review |
Flags
| Flag | Description |
|---|---|
--fix | Interactively fix email labels |
--dry-run | Show what would change without modifying anything |
sync-gnome-contacts
Sync Google Contacts to GNOME Contacts (Evolution Data Server).
Usage
rscontacts sync-gnome-contacts
rscontacts sync-gnome-contacts --dry-run
Flags
| Flag | Description |
|---|---|
--dry-run | Show what would change without modifying anything |
Configuration
rscontacts uses an optional TOML configuration file located at:
~/.config/rscontacts/config.toml
This is the same directory where OAuth credentials and the token cache are stored.
If the file does not exist, rscontacts runs with default settings (all checks enabled). If the file exists but contains errors, a warning is printed and defaults are used.
You can generate a default config file with rscontacts init-config. Use --force to overwrite an existing file.
check-all skip list
The [check-all] section controls which checks are included when running check-all. By default, all checks run. You can skip specific checks by listing them in the skip array:
[check-all]
skip = [
"check-contact-given-name-regexp",
"check-contact-label-nophone",
]
Skipped checks will not run and will not appear in the --stats output.
Individual check commands (e.g., rscontacts check-contact-given-name-regexp) are not affected by the config file and will always run when invoked directly.
Available check names
The following check names can be used in the skip list:
| Check name | Description |
|---|---|
check-phone-countrycode | Phone numbers missing a country code |
check-phone-format | Phone numbers not in +CC-NUMBER format |
check-contact-given-name-regexp | Given names not matching the configured allow regex |
check-contact-family-name-regexp | Family names not matching the configured allow regex |
check-contact-suffix-regexp | Suffixes not matching the allow regex (default: numeric) |
check-contact-no-given-name | Contacts with family name but no given name |
check-contact-no-identity | Contacts with no type tag |
check-contact-given-name-known | Given name not in the configured allowed names list |
check-contact-given-name-exists | Configured given names that have no matching contacts |
check-contact-company-known | Company field not in configured companies list |
check-contact-company-exists | Configured companies that have no matching contacts |
check-contact-displayname-duplicate | Multiple contacts with the same display name |
check-contact-no-displayname | Contacts with empty display name |
check-contact-type | Contacts missing or having both type:Person/type:Company labels |
check-contact-type-company-no-company | Company-tagged contacts without a company field |
check-contact-type-company-given-name | Company-tagged contacts with given name != company field |
check-contact-type-company-no-label | Company-tagged contacts missing company:<name> label |
check-contact-no-middle-name | Contacts with a middle name set |
check-contact-no-nickname | Contacts with a nickname set |
check-contact-no-label | Contacts not assigned to any label |
check-phone-label-missing | Phone numbers without a label (mobile/home/work) |
check-phone-label-english | Non-English phone labels |
check-phone-country-label | Missing or wrong country labels for phone numbers |
check-contact-email | Invalid or uppercase email addresses |
check-phone-duplicate | Duplicate phone numbers on a contact |
check-contact-email-duplicate | Duplicate email addresses on a contact |
check-contact-label-nophone | Empty labels (contact groups with no members) |
check-contact-label-regexp | Labels not matching the configured allow regex |
Config sections
[check-all]
Controls which checks to skip when running check-all.
[check-all]
skip = ["check-contact-no-label", "check-phone-label-missing"]
[check-contact-given-name-regexp]
Allowlist regex for given names. Contacts whose given name does not match this pattern are flagged. If no allow regex is configured, the check is silently skipped in check-all.
[check-contact-given-name-regexp]
allow = '^[A-Z][a-z]*$'
[check-contact-family-name-regexp]
Allowlist regex for family names. Same behavior as the given name regex check.
[check-contact-family-name-regexp]
allow = '^([A-Z][a-z]+(-[A-Z][a-z]+)*|[1-9]\d*)$'
[check-contact-suffix-regexp]
Allowlist regex for name suffixes. If not configured, the check uses a default behavior that allows numeric suffixes.
[check-contact-suffix-regexp]
allow = '^[1-9]\d*$'
[check-contact-label-regexp]
Allowlist regex for contact labels (groups). Labels that do not match this pattern are flagged. If not configured, the check is silently skipped in check-all.
[check-contact-label-regexp]
allow = '^(type|company|person|service|group|organization):[A-Z][a-zA-Z]*$'
[check-contact-name-is-company]
List of known company names. Used by check-contact-company-known (flags contacts whose company field is not in this list) and check-contact-company-exists (flags configured companies that have no matching contacts). If the list is empty, both checks are skipped in check-all.
[check-contact-name-is-company]
companies = ["Google", "Microsoft", "Amazon"]
[check-contact-given-name-known]
List of allowed given names (case-sensitive). Used by check-contact-given-name-known (flags contacts whose given name is not in this list) and check-contact-given-name-exists (flags configured names that have no matching contacts). If the list is empty, check-contact-given-name-exists is skipped in check-all.
[check-contact-given-name-known]
names = ["John", "Jane", "Mark"]
Regex syntax
The allow values use Rust regex syntax. For example, '^[A-Z][a-z]+$' requires values to start with an uppercase letter followed by one or more lowercase letters. Names like “Smith” pass, while “smith”, “SMITH”, “Smith 2”, or “123” would be flagged.
Name regexp checks support --fix for interactive fixing (rename/delete/skip, plus swap for given name). The label regexp check supports --fix for interactive renaming.
Example configuration
# ~/.config/rscontacts/config.toml
[check-all]
skip = [
"check-contact-given-name-regexp",
"check-contact-no-label",
"check-phone-label-missing",
]
[check-contact-given-name-regexp]
allow = '^[A-Z][a-z]*$'
[check-contact-family-name-regexp]
allow = '^([A-Z][a-z]+(-[A-Z][a-z]+)*|[1-9]\d*)$'
[check-contact-label-regexp]
allow = '^(type|company|person|service|group|organization):[A-Z][a-zA-Z]*$'
[check-contact-name-is-company]
companies = ["Google", "Microsoft"]
[check-contact-given-name-known]
names = ["John", "Jane", "Mark"]
Same Name Issue
The Problem
You may have multiple contacts with the same name. For example, two friends both called “Mike” whose family names you don’t know. The check-contact-name-duplicate command will flag these as issues, but how should you actually resolve them?
What Google Contacts Shows in the List View
The contact list (on both the phone app and web UI) only shows a few fields at a glance:
- Name — always visible
- Photo/avatar — if one is set
- Organization — displayed as a subtitle under the name
All other fields (phone number, email, labels, notes, etc.) are only visible after tapping into the contact’s detail view. This means that when you’re scanning your contact list or picking a contact to call, only the name, photo, and organization can help you tell two people apart.
No Separate Display Name
Google Contacts does not support a separate “display name” field. The display_name in the People API is a read-only computed field that is automatically generated from the structured name fields (given name, family name, etc.). You cannot set a display name independently of the actual name.
The “File As” Field
Google Contacts has a “File as” field (fileAses in the People API), but it is a sorting hint, not a display name. It controls where the contact appears in an alphabetically sorted list, but the contact list still shows the actual name. Setting different “File as” values (e.g., “Mike Gym”, “Mike Work”) would affect sort order but would not help you visually distinguish same-name contacts in the list view.
Options for Distinguishing Same-Name Contacts
1. Modify the Name
Add a distinguishing suffix or identifier to the name itself:
- “Mike R.” / “Mike S.”
- “Mike (work)” / “Mike (gym)”
- “Mike Tel Aviv” / “Mike Neighbor”
This is the most visible approach since the name is always shown, but it means the stored name no longer reflects the person’s real name.
2. Use the Organization Field
Set the organization/company field to something descriptive (e.g., “gym”, “yoga class”, “neighbor”). This shows as a subtitle under the name in the contact list, so you can distinguish contacts without changing the name itself. This is the cleanest approach if you want to keep the real name intact.
Note: the organization field in the API (organizations) has multiple sub-fields — company name, job title, and department — but only the company name is displayed in the contact list view. Title and department are only visible in the contact detail view. In the Google Contacts UI, this field is labeled “Company”. So when distinguishing same-name contacts, make sure to set the company name, not just the title.
3. Use the Name Suffix Field
The People API’s Name resource has an honorific_suffix field (intended for “Jr.”, “III”, “PhD”, etc.). You could set it to “1”, “2”, “3” or other short identifiers to distinguish same-name contacts. The suffix gets incorporated into the computed display name, so “Mike” with suffix “1” would display as “Mike 1”. This works, but it’s a hack — the field is meant for real honorific suffixes, not arbitrary identifiers. It’s functionally equivalent to just modifying the name directly.
4. Set Different Photos
If you have photos of both people, assigning different contact photos makes them instantly recognizable in the list view.
5. Leave Them as Duplicates
You can simply leave both contacts as “Mike” and accept the warning from check-contact-name-duplicate. Currently rscontacts has no mechanism to suppress or allowlist known duplicates, so this check will continue to flag them on every run.
Recommendation
Using the organization field is generally the best compromise: it keeps the real name intact while providing a visible subtitle in the contact list. If that’s not enough, modifying the name with a short qualifier is the most reliable fallback.
Transport Errors
The Google People API occasionally returns transient HTTP errors that are not caused by anything wrong with your request. These are server-side issues that typically resolve themselves after a short wait.
Error example
Here is an example of a 502 Bad Gateway error returned by the Google People API:
Error: Failure(Response { status: 502, version: HTTP/2.0, headers: {"content-type": "text/html; charset=UTF-8", "referrer-policy": "no-referrer", "content-length": "1613", "date": "Wed, 11 Mar 2026 08:49:16 GMT", "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"}, body: BoxBody })
Retried status codes
rscontacts automatically retries API calls that fail with any of these HTTP status codes:
| Code | Meaning | Typical cause |
|---|---|---|
| 429 | Too Many Requests | Rate limit exceeded |
| 502 | Bad Gateway | Google backend overloaded or timed out |
| 503 | Service Unavailable | Temporary Google service disruption |
| 504 | Gateway Timeout | Google backend did not respond in time |
Retry behavior
When a transient error is detected, rscontacts retries the request up to 3 times with exponential backoff delays:
- First retry after 1 second
- Second retry after 2 seconds
- Third retry after 4 seconds
If all 3 retries fail, the error is propagated to the user as usual.
The --transport-errors flag
By default, retries happen silently. To see when retries occur, pass the
--transport-errors flag to any command:
rscontacts --transport-errors list
rscontacts --transport-errors all-checks --fix
When a retry is triggered, a message is printed to stderr:
[transport] HTTP 502 Bad Gateway - retrying in 1s (attempt 1/3)
This flag is useful for debugging connectivity issues or monitoring API reliability.
Phone Sync Issues
Sometimes your Google Contacts data goes out of sync with what is actually
displayed on your phone. You may notice missing contacts, stale phone numbers,
or contacts that you have already fixed via rscontacts still showing their
old values on the device.
Symptoms
- Contacts on your phone do not match what you see in Google Contacts on the web.
- Changes made through
rscontacts --fixor the Google Contacts web UI are not reflected on your phone. - Duplicate or outdated entries keep appearing on the device.
Solution
-
Back up your contacts. Export your contacts from Google Contacts (or use
rscontacts list) so you have a safe copy before making any changes on the device. -
Open your phone’s Settings and navigate to Apps (or Application Manager, depending on your Android version).
-
Show system apps. Tap the three-dot menu (or filter) and enable “Show system apps” so that hidden system applications are visible.
-
Find “Contacts Storage”. This is the system app that caches contact data locally on the device. It is separate from the “Contacts” app you normally use.
-
Clear cache and data. Open “Contacts Storage”, then:
- Tap Clear Cache.
- Tap Clear Data (or Clear Storage).
-
Wait for re-sync. Your phone will re-download all contact data from Google. This may take a few minutes depending on how many contacts you have. Once the sync completes, your phone contacts should match the server and the issue will be resolved.
Note: Clearing the data of Contacts Storage only removes the local cache. Your actual contacts remain safe in your Google account and will sync back automatically.
Design Decisions
Country Code Detection: Hardcoded Table vs phonenumber Crate
The check-phone-format command needs to split phone numbers into country code and local number (e.g., +79268335991 → +7-9268335991). Country codes vary in length: 1 digit (+1 US, +7 Russia), 2 digits (+44 UK), or 3 digits (+972 Israel).
Approach Chosen: Hardcoded ITU Country Code Table
rscontacts embeds a static list of ~190 ITU country codes and uses longest-prefix matching (3 digits, then 2, then 1) to detect the country code boundary.
Alternative Considered: phonenumber Crate
The phonenumber crate is a Rust port of Google’s libphonenumber. It provides:
- Phone number validation (correct digit count per country)
- Multiple formatting options (E.164, international, national)
- Region detection from number
Why We Chose the Hardcoded Table
- We only need one thing: splitting the country code from the local number. The crate would be overkill.
- Minimal dependency: the crate bundles ~2MB of per-country metadata for phone number rules we don’t use.
- Simplicity: a
constarray with prefix matching is trivial to understand and maintain.
When to Reconsider
If rscontacts ever needs to validate that a phone number has the correct number of digits for its country, or needs national formatting, the phonenumber crate would be the right choice.
Future Ideas
This page collects ideas for future development of rscontacts.
Web Application
Turn rscontacts into a web application that provides a browser-based UI for auditing and fixing Google Contacts. This would make the tool accessible to non-technical users who are not comfortable with the command line. The web app could:
- Display all check results in a dashboard view with counts and summaries.
- Allow users to review and approve fixes one by one or in bulk.
- Authenticate via OAuth in the browser instead of requiring a local credentials file.
- Provide real-time progress indicators for long-running checks.
- Use a Rust web framework (e.g., Axum or Actix-web) for the backend, reusing the existing check and fix logic from the CLI.
- Use a lightweight frontend (e.g., HTMX, or a simple SPA with a framework like Leptos or Yew for a full-Rust stack).
Distinguish Companies from Individuals
Currently there is no way to tell whether a contact represents a company or an individual person. Knowing this distinction would allow checks to apply different rules (e.g., company contacts would not need a given/family name split, and name format checks like capitalization or suffix numbering would not apply).
One approach: maintain a local file listing known company names. A check could then compare each contact’s organization field (or display name) against this list and flag ambiguous entries. Contacts matching a known company name could be tagged or moved into a dedicated label/group.
Another approach: use a heuristic — contacts that have an organization name but no given/family name are likely companies. This could be combined with the company-names file for better accuracy.
Better Contact Organization
Based on analysis of typical contact databases, here are ideas for improving contact organization:
Merge Duplicate Contacts
Contacts that share multiple phone numbers are almost certainly duplicates and should be merged. A check could detect contacts sharing phone numbers and prompt the user to merge them.
Enrich Organization Field
Most contacts lack an organization field. Contacts with corporate email domains (e.g., @johnbryce.co.il, @sqlink.com) could have their organization auto-populated from the domain name.
Group by Employer/Domain
Create labels automatically based on email domains for professional contacts. For example, all contacts with @example.com emails could be auto-labeled company:Example.
Group by Country (implemented)
Contacts can be auto-labeled by country based on their phone number country codes (e.g., country:Israel for +972, country:Ukraine for +380). See check-phone-country-label. This works both ways: missing labels are added, and stale labels (where no phone number matches) are removed.
Clean Unreachable Contacts
Contacts with a name but no phone number and no email are unreachable and should be enriched or removed.
Add Birthdays
Typically very few contacts have birthdays set. Adding birthdays to important contacts enables birthday reminders.
Break Up Large Groups
If a single label contains the vast majority of contacts, it is too broad to be useful. Consider splitting it by relationship type (family, friends, colleagues, service providers, etc.).
Add Missing Emails
Many contacts have only a phone number. For contacts with an organization, it may be possible to infer or look up work email addresses.
Performance Improvements
Set page_size on API fetches
fetch_all_contacts and fetch_all_contact_groups don’t set page_size, using the API default (which may be small). Setting it to the maximum (1000 for contacts, 200 for groups) would reduce the number of round trips to Google’s API.
Robustness Improvements
Improve label creation error messages
When label creation fails mid-fix (e.g., in check-contact-no-label --fix or check-phone-country-label --fix), the error message doesn’t indicate which contact and label were involved. Including the contact name and target label in the error would make debugging easier.
Proactive stale token detection
When the OAuth token is expired or corrupted, API calls currently wait for the 30-second request timeout before failing. The tool could proactively check token freshness (e.g., by inspecting the expiry_date field in token_cache.json) and suggest rscontacts auth immediately instead of waiting for timeouts.