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

Run All Checks

Name Checks

Company Checks

Phone Checks

Email Checks

Contact Group (Label) Checks

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

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.

init-config

Generate a default config file at ~/.config/rscontacts/config.toml.

Usage

rscontacts init-config
rscontacts init-config --force

Flags

FlagDescription
--forceOverwrite 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

FlagDescription
--emailsAlso show email addresses
--labelsAlso show contact labels (contact group memberships)
--starredOnly 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

ArgumentDescription
nameName (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

FlagDescription
--shortExport 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

FlagDescription
--fixInteractively fix all issues found
--dry-runShow what would change without modifying anything
--statsOnly show error counts per check, no details
--verbose, -vShow 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

FlagDescription
--fixInteractively fix non-matching given names
--dry-runShow 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

FlagDescription
--fixInteractively fix non-matching family names
--dry-runShow 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

FlagDescription
--fixInteractively fix non-matching suffixes
--dry-runShow 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

FlagDescription
--fixInteractively fix duplicate display names
--dry-runShow 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

FlagDescription
--fixInteractively fix contacts with empty display names
--dry-runShow 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

FlagDescription
--fixInteractively fix contacts missing a given name
--dry-runShow 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

FlagDescription
--fixInteractively fix contacts with middle names
--dry-runShow 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

FlagDescription
--fixInteractively fix contacts with nicknames
--dry-runShow 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

FlagDescription
--fixInteractively fix unknown given names
--dry-runShow 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

FlagDescription
--fixInteractively fix given names with no contacts
--dry-runShow 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

FlagDescription
--fixInteractively fix contacts with unknown companies
--dry-runShow 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

FlagDescription
--fixInteractively fix companies with no contacts
--dry-runShow 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

FlagDescription
--fixInteractively fix contacts with missing or duplicate type labels
--dry-runShow 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

FlagDescription
--fixInteractively assign a type tag to untagged contacts
--dry-runShow 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

FlagDescription
--fixInteractively fix mismatched given names
--auto-fixAutomatically set given name to match company field
--dry-runShow 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

FlagDescription
--fixInteractively fix company contacts missing the company field
--dry-runShow 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

FlagDescription
--fixInteractively fix missing company labels
--auto-fixAutomatically create and assign the company label
--dry-runShow 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

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

FlagDescription
--fixInteractively fix duplicate phone entries
--dry-runShow 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

FlagDescription
--fixInteractively assign labels to unlabeled phone numbers
--dry-runShow 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:

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

FlagDescription
--fixInteractively fix invalid or uppercase emails
--dry-runShow 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

FlagDescription
--fixInteractively fix duplicate email entries
--dry-runShow 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

FlagDescription
--fixInteractively fix empty labels
--dry-runShow 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

FlagDescription
--fixInteractively fix non-matching labels
--dry-runShow 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:

  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.

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.

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

FlagDescription
--dry-runShow 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

FlagDescription
--dry-runShow 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

FlagDescription
--dry-runShow 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

ArgumentDescription
labelThe label (contact group) name to remove

Flags

FlagDescription
--dry-runShow 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

ArgumentDescription
labelThe phone label to review

Flags

FlagDescription
--fixInteractively fix phone labels
--dry-runShow 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

ArgumentDescription
labelThe email label to review

Flags

FlagDescription
--fixInteractively fix email labels
--dry-runShow 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

FlagDescription
--dry-runShow 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 nameDescription
check-phone-countrycodePhone numbers missing a country code
check-phone-formatPhone numbers not in +CC-NUMBER format
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-no-given-nameContacts with family name but no given name
check-contact-no-identityContacts with no type tag
check-contact-given-name-knownGiven name not in the configured allowed names list
check-contact-given-name-existsConfigured given names that have no matching contacts
check-contact-company-knownCompany field not in configured companies list
check-contact-company-existsConfigured companies that have no matching contacts
check-contact-displayname-duplicateMultiple contacts with the same display name
check-contact-no-displaynameContacts with empty display name
check-contact-typeContacts missing or having both type:Person/type:Company labels
check-contact-type-company-no-companyCompany-tagged contacts without a company field
check-contact-type-company-given-nameCompany-tagged contacts with given name != company field
check-contact-type-company-no-labelCompany-tagged contacts missing company:<name> label
check-contact-no-middle-nameContacts with a middle name set
check-contact-no-nicknameContacts with a nickname set
check-contact-no-labelContacts not assigned to any label
check-phone-label-missingPhone numbers without a label (mobile/home/work)
check-phone-label-englishNon-English phone labels
check-phone-country-labelMissing or wrong country labels for phone numbers
check-contact-emailInvalid or uppercase email addresses
check-phone-duplicateDuplicate phone numbers on a contact
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

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:

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.

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.