Author: Eepsita Modi
This project is a Python-based automation system that reads real incoming emails from a Gmail account using the Gmail API, extracts relevant information, and appends the data into a Google Sheet using the Google Sheets API.
The script is designed to be idempotent and safe to re-run, ensuring:
- Only new emails are processed
- No duplicate rows are added
- Emails are marked as read after successful processing
The project uses OAuth 2.0 (Desktop Application flow) as required by the assignment.
A short screen-recorded video demonstrating:
- Project flow
- Gmail → Google Sheets data movement
- Duplicate prevention logic
- Behavior when the script is run multiple times
📎 Video Link: https://drive.google.com/file/d/15ElnhBzoC9xZYlccfGfz492qdxRRMy0d/view?usp=sharing
gmail-to-sheets/
├── credentials/
│ └── credentials.json # OAuth client credentials (NOT committed)
├── src/
│ ├── main.py # Orchestration logic
│ ├── gmail_service.py # Gmail OAuth & API setup
│ ├── sheets_service.py # Google Sheets operations
│ ├── email_parser.py # Email parsing & HTML cleanup
│ └── config.py # Central configuration
├── state/
│ ├── token.json # OAuth token (NOT committed)
│ └── last_processed.txt # Persistent processing state (NOT committed)
├── requirements.txt
├── .gitignore
└── README.md
- Uses Google OAuth 2.0 Desktop Application flow
- On first run, a browser window opens for user consent
- OAuth access token is stored locally in
state/token.json - Subsequent runs reuse the stored token automatically
Security:
OAuth tokens and credentials are explicitly excluded from version control using .gitignore.
Each processed email is appended as a new row with the following columns:
From | Subject | Date | Content
- Headers are automatically created if the sheet is empty
- Email content is truncated to avoid excessively large cells
- Data is appended (never overwritten)
Without state management:
- Emails would be reprocessed on every run
- Duplicate rows would be added to the Google Sheet
State is stored locally in:
state/last_processed.txt
Format:
<timestamp>|<message_id>
Example:
1705209142000|18c7f2a1b4e3d912
timestamp→ GmailinternalDate(milliseconds)message_id→ Unique Gmail message ID
Before processing each email:
- Emails older than the stored timestamp are skipped
- Emails with the same timestamp but lower or equal message IDs are skipped
This guarantees:
- No duplicate rows
- Correct handling of same-timestamp emails
- Safe re-execution of the script
Duplicate prevention is achieved using three layers:
- Gmail query fetches unread emails only
- Persistent state file prevents reprocessing old messages
- Emails are marked as read only after successful append
This ensures idempotent behavior across multiple runs.
Handling duplicate emails when the script is re-run, especially when multiple emails share the same timestamp.
This was solved by implementing a persistent state mechanism that stores both the timestamp and message ID of the last processed email. This ensures precise ordering and prevents duplicates even in edge cases.
The script uses structured logging in the format:
YYYY-MM-DD HH:MM:SS | LEVEL | Message
Examples:
Fetching unread messagesAppending 3 new emails to Google SheetsNo new emails since last run
This improves debugging and traceability.
pip install -r requirements.txtPlace your Google OAuth client file at:
credentials/credentials.json
python src/main.py- Email attachments are not processed
- Inline images are ignored
- The script runs as a batch job, not a real-time service
- Designed for a single Gmail account
These limitations were intentional to keep the solution focused and aligned with the assignment scope.
This project demonstrates:
- Correct usage of Gmail & Google Sheets APIs
- Secure OAuth 2.0 authentication
- Reliable state persistence
- Duplicate-safe, repeatable automation
- Clean, modular Python architecture
