🚀 Executive Summary
TL;DR: Manually syncing ProtonMail contacts to Google Contacts is a recurring, time-consuming task. This guide provides a Python script-based solution to automate the export of ProtonMail vCard files and push them to Google Contacts via the Google People API, creating a ‘set it and forget it’ workflow.
🎯 Key Takeaways
- The sync workflow involves exporting a `vcf` file from ProtonMail, enabling the Google People API in a GCP project, and setting up an OAuth 2.0 ‘Desktop app’ client ID.
- A Python script utilizes `vobject` to parse the `contacts.vcf` file and `google-api-python-client` to authenticate and create new contacts in Google via the `people.createContact` endpoint.
- Authentication for the Google People API is managed by `google-auth-oauthlib`, generating a `token.json` file after the initial browser-based authorization, which stores credentials for subsequent script runs.
Syncing ProtonMail Contacts to Google Contacts (vCard)
Hey team, Darian here. Let’s talk about a small but persistent friction point: contact management. For the longest time, my professional contacts lived exclusively in ProtonMail, while my personal ones were in Google. This meant my phone (synced to Google) wouldn’t recognize a call from a new client. I used to manually update contacts on both platforms, and I finally calculated I was wasting about an hour a month on it. That’s a non-starter.
This tutorial outlines a straightforward, script-based workflow to sync your ProtonMail contacts over to Google. It’s a “set it and forget it” solution that saves you from tedious manual entry. We’re going to export a vCard file from Proton and use a Python script to push it to the Google People API. Let’s get it done.
Prerequisites
Before we dive in, make sure you have the following ready:
- A ProtonMail account with contacts you want to export.
- A Google account and a corresponding Google Cloud Platform (GCP) project.
- Python 3.x installed on your machine.
- Familiarity with enabling APIs in the GCP console.
The Guide: Step-by-Step
Step 1: Export Contacts from ProtonMail
First, we need the source data. Proton makes this easy.
- Log in to your ProtonMail account on the web.
- Navigate to the Contacts section (the address book icon).
- In the left sidebar, click the gear icon to open Settings.
- Click the Export Contacts button. This will download a file named
protonmail-contacts.vcf. - For simplicity, rename this file to
contacts.vcfand place it in your project directory.
This is the manual part of the process. You’ll need to do this whenever you want to run a fresh sync.
Step 2: Configure Google People API Access
Now, let’s get the credentials our script needs to talk to Google.
- Go to your Google Cloud Console and select the project you want to use.
- In the navigation menu, go to APIs & Services > Library. Search for “Google People API” and enable it.
- Next, navigate to APIs & Services > OAuth consent screen. Configure it for an “External” user type, fill in the required app information (app name, user support email), and save. You don’t need to submit for verification for personal use.
- Go to APIs & Services > Credentials. Click + CREATE CREDENTIALS and select OAuth client ID.
- Choose “Desktop app” as the application type. Give it a name like “Contacts Sync Script”.
- After creation, a modal will pop up. Click DOWNLOAD JSON. This will download a
client_secret_....jsonfile. - Rename this file to
credentials.jsonand place it in the same project directory as yourcontacts.vcffile. Treat this file like a password; do not commit it to version control.
Pro Tip: In my production setups, I manage secrets like
credentials.jsonusing a proper secrets manager (like HashiCorp Vault or AWS Secrets Manager) and pull them into the environment at runtime. For a personal script, keeping it in a git-ignored local directory is acceptable.
Step 3: The Python Script
Here’s where the magic happens. We’ll write a script to parse the VCF file and create the contacts in Google.
First, you’ll need a few Python libraries. I’ll skip the standard virtualenv setup since you likely have your own workflow for that. Just make sure you install these packages for your environment using pip:
google-api-python-client, google-auth-httplib2, google-auth-oauthlib, vobject
Create a Python file, let’s call it sync_contacts.py, and add the following code:
import os.path
import vobject
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/contacts']
VCF_FILE_PATH = 'contacts.vcf'
CREDENTIALS_FILE = 'credentials.json'
TOKEN_FILE = 'token.json'
def get_google_service():
"""Authenticates with Google and returns a service object."""
creds = None
if os.path.exists(TOKEN_FILE):
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
try:
creds.refresh(Request())
except Exception as e:
print(f"Token refresh failed: {e}. Re-authenticating...")
flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES)
creds = flow.run_local_server(port=0)
else:
flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open(TOKEN_FILE, 'w') as token:
token.write(creds.to_json())
try:
service = build('people', 'v1', credentials=creds)
return service
except HttpError as err:
print(f"An error occurred building the service: {err}")
return None
def parse_vcard(vcard_path):
"""Parses a VCF file and yields contact information."""
if not os.path.exists(vcard_path):
print(f"Error: VCF file not found at {vcard_path}")
return
with open(vcard_path, 'r', encoding='utf-8') as f:
vcard_data = f.read()
for vcard in vobject.readComponents(vcard_data):
try:
# Extract formatted name
full_name = vcard.fn.value if hasattr(vcard, 'fn') else "No Name"
# Extract email, preferring WORK or HOME
email = None
if hasattr(vcard, 'email'):
email = vcard.email.value
# Extract phone number
phone_number = None
if hasattr(vcard, 'tel'):
phone_number = vcard.tel.value
contact_info = {
'name': full_name.strip(),
'email': email.strip() if email else None,
'phone': phone_number.strip() if phone_number else None
}
# Skip entries that have no useful data
if contact_info['name'] != "No Name" or contact_info['email'] or contact_info['phone']:
yield contact_info
except Exception as e:
print(f"Skipping a malformed vCard entry: {e}")
continue
def create_google_contact(service, contact_info):
"""Creates a single contact in Google Contacts."""
try:
person_body = {
"names": [{"givenName": contact_info['name']}],
"emailAddresses": [{"value": contact_info['email']}] if contact_info['email'] else [],
"phoneNumbers": [{"value": contact_info['phone'], "type": "mobile"}] if contact_info['phone'] else []
}
# Here we could add a check to see if the contact already exists
# For simplicity, we are just creating them. Be mindful of duplicates.
service.people().createContact(body=person_body).execute()
print(f"Successfully created contact: {contact_info['name']}")
except HttpError as err:
print(f"An error occurred creating contact {contact_info['name']}: {err}")
except Exception as e:
print(f"A general error occurred for {contact_info['name']}: {e}")
def main():
print("Starting contact sync...")
service = get_google_service()
if not service:
print("Could not authenticate with Google. Exiting.")
return
contacts_to_create = parse_vcard(VCF_FILE_PATH)
count = 0
for contact in contacts_to_create:
create_google_contact(service, contact)
count += 1
print(f"\nSync complete. Processed {count} contacts.")
if __name__ == '__main__':
main()
Logic Breakdown:
- get_google_service(): This is the authentication handler. The first time you run the script, it will open a browser window asking you to authorize access to your Google account. It then saves an authentication token in
token.jsonso you don’t have to log in every time. - parse_vcard(): This function opens our
contacts.vcf, reads the data, and uses thevobjectlibrary to parse it into a structured format. It yields a simple dictionary for each contact with their name, email, and phone. - create_google_contact(): This takes the parsed contact info and constructs the request body that the Google People API expects. It then calls the
people.createContactendpoint to create the contact. - main(): The main function orchestrates the process: get the authenticated service, parse the VCF, and loop through each contact to create it in Google.
Step 4: Running and Automating the Sync
To run the script, simply open your terminal in the project directory and execute:
python3 sync_contacts.py
The first time, you’ll go through the browser authentication. Subsequent runs will use the token.json file.
Pro Tip on Automation: To make this a true “set it and forget it” solution, you can schedule it with cron. For instance, to run it at 2 AM every Monday, you would add a line like this to your crontab. Note that you need to place a fresh
contacts.vcffile in the directory before it runs. The command would be as simple as `0 2 * * 1 python3 sync_contacts.py`, assuming your cron environment is set up to find your script.
Common Pitfalls
I’ve hit a few walls setting this up before, so here are some things to watch out for:
- API Scopes: The script uses
https://www.googleapis.com/auth/contacts. If you change this to a read-only scope, the script will fail with a permissions error. If you change the scopes, you must deletetoken.jsonand re-authenticate. - Duplicate Contacts: This script is simple and doesn’t check for existing contacts before creating new ones. If you run it multiple times with the same VCF file, you’ll get duplicates. A more advanced version would first query the People API to see if a contact with the same email address already exists.
- Expired Tokens: Rarely, the refresh token in
token.jsoncan become invalid. If you repeatedly get authentication errors, the quickest fix is to deletetoken.jsonand let the script re-authenticate from scratch.
Conclusion
And that’s it. You now have a robust script that bridges the gap between your ProtonMail and Google contacts. It’s a small piece of automation that removes a recurring manual task, freeing you up to focus on more important things. Feel free to expand on it—add logic to handle custom fields, update existing contacts, or even pull from other sources. Happy scripting.
– Darian Vance
🤖 Frequently Asked Questions
âť“ How can I automate the synchronization of my ProtonMail contacts with Google Contacts?
Automate by exporting contacts from ProtonMail as a vCard (`.vcf`), configuring Google People API access with an OAuth client ID in Google Cloud Platform, and running a Python script that parses the vCard and uses the Google People API to create contacts.
âť“ How does this script-based method compare to manual syncing or other third-party tools?
This script-based method offers a controlled, ‘set it and forget it’ automation for one-way syncing, eliminating manual entry. Unlike some third-party tools, it provides direct control over data transfer using official APIs, reducing reliance on external services.
âť“ What are the key issues to watch out for when implementing this ProtonMail to Google Contacts sync?
Key issues include ensuring correct API scopes (e.g., `https://www.googleapis.com/auth/contacts`) and deleting `token.json` if scopes change, managing potential duplicate contacts as the script lacks de-duplication logic, and handling expired `token.json` files by re-authenticating.
Leave a Reply