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:
Listing & Inspection
| Command | Description |
|---|---|
| list | List all contacts |
| show-contact | Show all details about a specific contact |
| edit-contact | Interactively edit a contact |
| show-phone-labels | Show all distinct phone labels in use |
| show-contact-labels | Show all contact groups with member counts |
Check Commands
All check commands are also run by check-all.
Name Checks
| Command | --fix | Description |
|---|---|---|
| check-contact-given-name-regexp | Yes | Given names not matching allow regex |
| check-contact-family-name-regexp | Yes | Family names not matching allow regex |
| check-contact-suffix-regexp | Yes | Suffixes not matching allow regex |
| check-contact-displayname-duplicate | Yes | Duplicate contact display names |
| check-contact-name-is-company | Yes | Given/family name matches a company name |
Company Checks
| Command | --fix | Description |
|---|---|---|
| check-contact-company-known | Yes | Company field not in configured companies list |
Phone Checks
| Command | --fix | Description |
|---|---|---|
| check-phone-countrycode | Yes | Missing country code |
| check-phone-format | Yes | Not in +CC-NUMBER format |
| check-phone-label-missing | Yes | Missing phone type label |
| check-phone-label-english | Yes | Non-English phone labels |
| check-phone-duplicate | Yes | Same phone attached twice |
Email Checks
| Command | --fix | Description |
|---|---|---|
| check-contact-email | Yes | Invalid or uppercase email addresses |
| check-contact-email-duplicate | Yes | Same email attached twice |
Contact Group (Label) Checks
| Command | --fix | Description |
|---|---|---|
| check-contact-no-label | Yes | Contacts not in any group |
| check-contact-label-nophone | Yes | Empty contact groups |
| check-contact-label-regexp | Yes | Labels not matching allow regex |
Action Commands
| Command | Description |
|---|---|
| compact-suffixes-for-contacts | Compact suffixes for contacts sharing the same base name |
| review-phone-label | Review all phones with a specific label |
| remove-label-from-all-contacts | Remove a label from all contacts |
Other Commands
| Command | Description |
|---|---|
| auth | Authenticate with Google |
| init-config | Generate a default config file |
| version | Print version information |
| complete | Generate shell completions |
Common Flags
Most check commands support:
| Flag | Description |
|---|---|
--fix | Interactively fix issues found |
--dry-run | Show what would change without modifying anything |
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) |
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)
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
show-contact-labels
Show all contact groups (labels) with their member counts.
Usage
rscontacts show-contact-labels
Output
Friends (12)
Family (5)
Work (23)
check-all
Run all checks at once.
Usage
rscontacts check-all
rscontacts check-all --fix
rscontacts check-all --fix --dry-run
rscontacts check-all --stats
rscontacts check-all --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 |
--country <CODE> | Country code for phone formatting (default: 972) |
Stats Mode
With --stats, only checks with errors are listed:
check-phone-format: 5
check-phone-label-english: 3
check-duplicate-emails: 1
---
Total: 9
Checks Run
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-name-english
Find contacts with non-English (non-ASCII) characters in their names.
Usage
rscontacts check-name-english
rscontacts check-name-english --fix
rscontacts check-name-english --fix --dry-run
Fix Behavior
With --fix, prompts for each contact: [r]ename / [d]elete / [s]kip.
check-name-caps
Find contacts with all-caps names (e.g., “JOHN DOE”).
Usage
rscontacts check-name-caps
rscontacts check-name-caps --fix
rscontacts check-name-caps --fix --dry-run
Fix Behavior
With --fix, prompts for each contact: [r]ename / [d]elete / [s]kip.
check-name-first-capital-letter
Find contacts whose name doesn’t start with a capital letter.
Usage
rscontacts check-name-first-capital-letter
rscontacts check-name-first-capital-letter --fix
rscontacts check-name-first-capital-letter --fix --dry-run
Fix Behavior
With --fix, prompts for each contact: [r]ename / [d]elete / [s]kip.
check-name-order
Find contacts with reversed name order (e.g., “Veltzer, Mark” instead of “Mark Veltzer”).
This happens when Google formats display_name with the family name first, typically for contacts created with a non-Latin locale.
Usage
rscontacts check-name-order
rscontacts check-name-order --fix
rscontacts check-name-order --fix --dry-run
Output
Veltzer, Mark -> Mark Veltzer
Fix Behavior
With --fix, prompts for each contact: [r]ename / [d]elete / [s]kip. When renaming, the suggested corrected name is offered as a default (press Enter to accept).
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-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-no-label
Find phone numbers that don’t have a type label (mobile, home, work, etc.).
Usage
rscontacts check-phone-no-label
rscontacts check-phone-no-label --fix
rscontacts check-phone-no-label --fix --dry-run
Fix Behavior
With --fix, prompts with predefined English label choices for each unlabeled phone:
Label for John's phone? [m]obile/[h]ome/[w]ork/m[a]in/[o]ther/[s]kip:
Notes
This checks the phone number’s type and formatted_type fields. These are the labels you see in Google Contacts next to each phone number (e.g., “Mobile”, “Home”, “Work”).
This is different from check-contact-no-label, which checks contact group memberships.
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-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-space
Find contact groups (labels) whose name contains spaces.
Usage
rscontacts check-contact-label-space
rscontacts check-contact-label-space --fix
rscontacts check-contact-label-space --fix --dry-run
Fix Behavior
With --fix, prompts for a new name for each group. The new name must not be empty or contain spaces.
New name for "My Friends" (or [s]kip):
check-email
Find contacts with invalid-looking email addresses.
Usage
rscontacts check-email
Validation Rules
An email is considered invalid if:
- It has no
@sign - The local part (before
@) is empty - The domain part (after
@) is empty or has no. - The TLD is less than 2 characters
check-email-caps
Find contacts with uppercase letters in their email addresses.
Email addresses are case-insensitive in practice, so Mark@Gmail.com should be mark@gmail.com.
Usage
rscontacts check-email-caps
rscontacts check-email-caps --fix
rscontacts check-email-caps --fix --dry-run
Fix Behavior
With --fix, automatically lowercases all email addresses for each affected contact. No interactive prompt since the fix is unambiguous.
check-duplicate-phones
Find contacts that have the same phone number attached more than once.
Usage
rscontacts check-duplicate-phones
rscontacts check-duplicate-phones --fix
rscontacts check-duplicate-phones --fix --dry-run
Fix Behavior
With --fix, prompts for each contact with duplicates:
Remove duplicate "+972-505665636" from John Doe? [y/n]
Keeps the first occurrence and removes subsequent duplicates.
check-duplicate-emails
Find contacts that have the same email address attached more than once.
Usage
rscontacts check-duplicate-emails
rscontacts check-duplicate-emails --fix
rscontacts check-duplicate-emails --fix --dry-run
Fix Behavior
With --fix, prompts for each contact with duplicates:
Remove duplicate "user@example.com" from John Doe? [y/n]
Keeps the first occurrence and removes subsequent duplicates.
check-labels-nophone
Find contact groups (labels) that have no contacts assigned to them.
Usage
rscontacts check-labels-nophone
rscontacts check-labels-nophone --fix
rscontacts check-labels-nophone --fix --dry-run
Fix Behavior
With --fix, prompts to delete each empty group:
Delete label "OldGroup"? [y/n]
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.
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.
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.
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
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
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.
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-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-name-is-company | Given or family name matches a known company name |
check-contact-company-known | Company field not in configured companies list |
check-contact-displayname-duplicate | Multiple contacts with the same display name |
check-contact-no-label | Contacts not assigned to any label |
check-contact-email | Invalid or uppercase email addresses |
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 |
check-phone-countrycode | Phone numbers missing a country code |
check-phone-format | Phone numbers not in +CC-NUMBER format |
check-phone-label-missing | Phone numbers without a label (mobile/home/work) |
check-phone-label-english | Non-English phone labels |
check-phone-duplicate | Duplicate phone numbers on a contact |
Name allow regexes
The check-contact-given-name-regexp, check-contact-family-name-regexp, and check-contact-label-regexp checks flag items whose value does not match the configured regex pattern. This is an allowlist approach — define what a valid value looks like, and anything that doesn’t match gets flagged.
[check-contact-given-name-regexp]
allow = '^[A-Z][a-z]+$'
[check-contact-family-name-regexp]
allow = '^[A-Z][a-z]+$'
[check-contact-label-regexp]
allow = '^[A-Z][a-z]+$'
The allow value is a Rust regex. The example above 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.
If no allow regex is configured, the check is silently skipped in check-all. When run directly, it prints a message about the missing config.
Name checks support --fix for interactive fixing (rename/delete/skip, plus swap for given name). The label 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]+$'
[check-contact-label-regexp]
allow = '^[A-Z][a-z]+$'
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.