Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 --fix support 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-NUMBER format
  • 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

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

  1. Go to Google Cloud Console
  2. Create a new project (or use an existing one)
  3. Enable the People API under APIs & Services
  4. 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
  5. 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

  1. You provide OAuth2 client credentials (a JSON file from Google Cloud Console)
  2. On first use, rscontacts opens your browser to get consent
  3. The access token is cached locally for future requests

Files

FileLocationPurpose
Credentials~/.config/rscontacts/credentials.jsonOAuth2 client ID and secret
Token cache~/.config/rscontacts/token_cache.jsonCached 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

CommandDescription
listList all contacts
show-contactShow all details about a specific contact
edit-contactInteractively edit a contact
show-phone-labelsShow all distinct phone labels in use
show-contact-labelsShow all contact groups with member counts

Check Commands

All check commands are also run by check-all.

Name Checks

Command--fixDescription
check-contact-given-name-regexpYesGiven names not matching allow regex
check-contact-family-name-regexpYesFamily names not matching allow regex
check-contact-suffix-regexpYesSuffixes not matching allow regex
check-contact-displayname-duplicateYesDuplicate contact display names
check-contact-name-is-companyYesGiven/family name matches a company name

Company Checks

Command--fixDescription
check-contact-company-knownYesCompany field not in configured companies list

Phone Checks

Command--fixDescription
check-phone-countrycodeYesMissing country code
check-phone-formatYesNot in +CC-NUMBER format
check-phone-label-missingYesMissing phone type label
check-phone-label-englishYesNon-English phone labels
check-phone-duplicateYesSame phone attached twice

Email Checks

Command--fixDescription
check-contact-emailYesInvalid or uppercase email addresses
check-contact-email-duplicateYesSame email attached twice

Contact Group (Label) Checks

Command--fixDescription
check-contact-no-labelYesContacts not in any group
check-contact-label-nophoneYesEmpty contact groups
check-contact-label-regexpYesLabels not matching allow regex

Action Commands

CommandDescription
compact-suffixes-for-contactsCompact suffixes for contacts sharing the same base name
review-phone-labelReview all phones with a specific label
remove-label-from-all-contactsRemove a label from all contacts

Other Commands

CommandDescription
authAuthenticate with Google
init-configGenerate a default config file
versionPrint version information
completeGenerate shell completions

Common Flags

Most check commands support:

FlagDescription
--fixInteractively fix issues found
--dry-runShow 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

FlagDescription
--emailsAlso show email addresses
--labelsAlso 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

FlagDescription
--fixInteractively fix all issues found
--dry-runShow what would change without modifying anything
--statsOnly 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:

  1. Missing country labels: If a contact has a phone number with country code +972 (Israel), they should have a country:Israel label. If the label is missing, it is flagged.
  2. Stale country labels: If a contact has a country:Russia label 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:

CodeCountryCodeCountry
+1USA+44UK
+7Russia+49Germany
+33France+86China
+34Spain+91India
+39Italy+380Ukraine
+41Switzerland+972Israel
+971UAE+966Saudi 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 with country: 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

FlagDescription
--fixAdd country code to matching phone numbers
--dry-runShow 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

FlagDescription
--fixReformat phone numbers to +CC-NUMBER
--dry-runShow 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
  • 00 prefix: 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:

  1. Merges phone numbers — adds any phones from source contacts not already on the target (compared by normalized digits)
  2. Merges email addresses — adds any emails from source contacts not already on the target (compared case-insensitively)
  3. Merges addresses — adds any addresses not already on the target (compared by formatted value)
  4. Merges organization — copies from source only if the target has no organization
  5. Merges birthdays — copies from source only if the target has no birthday
  6. Merges biographies — copies from source only if the target has no biography
  7. Copies labels — adds all contact group memberships from source contacts to the target
  8. 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-all since 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:

  1. Merges phone numbers — adds any phones from source contacts not already on the target (compared by normalized digits)
  2. Merges email addresses — adds any emails from source contacts not already on the target (compared case-insensitively)
  3. Merges addresses — adds any addresses not already on the target (compared by formatted value)
  4. Merges organization — copies from source only if the target has no organization
  5. Merges birthdays — copies from source only if the target has no birthday
  6. Merges biographies — copies from source only if the target has no biography
  7. Copies labels — adds all contact group memberships from source contacts to the target
  8. 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-all since 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

FlagDescription
--no-browserPrint the auth URL instead of opening a browser
--forceDelete 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 nameDescription
check-contact-given-name-regexpGiven names not matching the configured allow regex
check-contact-family-name-regexpFamily names not matching the configured allow regex
check-contact-suffix-regexpSuffixes not matching the allow regex (default: numeric)
check-contact-name-is-companyGiven or family name matches a known company name
check-contact-company-knownCompany field not in configured companies list
check-contact-displayname-duplicateMultiple contacts with the same display name
check-contact-no-labelContacts not assigned to any label
check-contact-emailInvalid or uppercase email addresses
check-contact-email-duplicateDuplicate email addresses on a contact
check-contact-label-nophoneEmpty labels (contact groups with no members)
check-contact-label-regexpLabels not matching the configured allow regex
check-phone-countrycodePhone numbers missing a country code
check-phone-formatPhone numbers not in +CC-NUMBER format
check-phone-label-missingPhone numbers without a label (mobile/home/work)
check-phone-label-englishNon-English phone labels
check-phone-duplicateDuplicate 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:

CodeMeaningTypical cause
429Too Many RequestsRate limit exceeded
502Bad GatewayGoogle backend overloaded or timed out
503Service UnavailableTemporary Google service disruption
504Gateway TimeoutGoogle 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:

  1. First retry after 1 second
  2. Second retry after 2 seconds
  3. 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 --fix or the Google Contacts web UI are not reflected on your phone.
  • Duplicate or outdated entries keep appearing on the device.

Solution

  1. 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.

  2. Open your phone’s Settings and navigate to Apps (or Application Manager, depending on your Android version).

  3. Show system apps. Tap the three-dot menu (or filter) and enable “Show system apps” so that hidden system applications are visible.

  4. 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.

  5. Clear cache and data. Open “Contacts Storage”, then:

    • Tap Clear Cache.
    • Tap Clear Data (or Clear Storage).
  6. 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 const array 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.