gmail-streamer 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "pip"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -0,0 +1,11 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ uses: tsilva/.github/.github/workflows/test.yml@main
@@ -0,0 +1,73 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - pyproject.toml
8
+ - src/gmail_streamer/__init__.py
9
+
10
+ permissions:
11
+ contents: write
12
+ id-token: write
13
+
14
+ concurrency:
15
+ group: release-${{ github.ref }}
16
+ cancel-in-progress: false
17
+
18
+ jobs:
19
+ test:
20
+ runs-on: ubuntu-latest
21
+ timeout-minutes: 10
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: astral-sh/setup-uv@v5
25
+ with:
26
+ enable-cache: true
27
+ cache-dependency-glob: "uv.lock"
28
+ - run: uv sync
29
+ - run: |
30
+ exit_code=0
31
+ uv run --with pytest pytest || exit_code=$?
32
+ if [ $exit_code -eq 5 ]; then
33
+ echo "No tests collected — skipping"
34
+ exit 0
35
+ fi
36
+ exit $exit_code
37
+
38
+ pii-scan:
39
+ uses: tsilva/.github/.github/workflows/pii-scan.yml@main
40
+
41
+ publish-pypi:
42
+ needs: [test, pii-scan]
43
+ runs-on: ubuntu-latest
44
+ timeout-minutes: 15
45
+ environment: pypi
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+ with:
49
+ fetch-depth: 0
50
+
51
+ - name: Check version
52
+ id: check
53
+ uses: tsilva/.github/.github/actions/check-version@main
54
+
55
+ - name: Setup uv
56
+ if: steps.check.outputs.tag_exists == 'false'
57
+ uses: astral-sh/setup-uv@v5
58
+ with:
59
+ enable-cache: true
60
+
61
+ - name: Build package
62
+ if: steps.check.outputs.tag_exists == 'false'
63
+ run: uv build
64
+
65
+ - name: Create release
66
+ if: steps.check.outputs.tag_exists == 'false'
67
+ env:
68
+ GH_TOKEN: ${{ github.token }}
69
+ run: gh release create ${{ steps.check.outputs.tag }} dist/* --generate-notes
70
+
71
+ - name: Publish to PyPI
72
+ if: steps.check.outputs.tag_exists == 'false'
73
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,22 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ dist/
4
+ build/
5
+ src/gmail_streamer/_build_info.py
6
+ *.pyc
7
+ .venv/
8
+ profiles/*/
9
+ !profiles/example/
10
+ profiles/example/token.json
11
+ profiles/example/state.json
12
+ profiles/example/credentials.json
13
+
14
+ logs/
15
+
16
+ # Managed by tsilva/.github
17
+ # Do not remove - synced automatically
18
+ .claude/
19
+ .env
20
+ .env.*
21
+ .DS_Store
22
+ node_modules/
@@ -0,0 +1,22 @@
1
+ {
2
+ "workbench.colorCustomizations": {
3
+ "activityBar.activeBackground": "#f8b688",
4
+ "activityBar.background": "#f8b688",
5
+ "activityBar.foreground": "#15202b",
6
+ "activityBar.inactiveForeground": "#15202b99",
7
+ "activityBarBadge.background": "#099743",
8
+ "activityBarBadge.foreground": "#e7e7e7",
9
+ "commandCenter.border": "#15202b99",
10
+ "sash.hoverBorder": "#f8b688",
11
+ "statusBar.background": "#f59858",
12
+ "statusBar.foreground": "#15202b",
13
+ "statusBarItem.hoverBackground": "#f27a28",
14
+ "statusBarItem.remoteBackground": "#f59858",
15
+ "statusBarItem.remoteForeground": "#15202b",
16
+ "titleBar.activeBackground": "#f59858",
17
+ "titleBar.activeForeground": "#15202b",
18
+ "titleBar.inactiveBackground": "#f5985899",
19
+ "titleBar.inactiveForeground": "#15202b99"
20
+ },
21
+ "peacock.color": "#f59858"
22
+ }
@@ -0,0 +1,48 @@
1
+ # CLAUDE.md
2
+
3
+ ## Project Overview
4
+
5
+ gmail-streamer is a Python CLI tool that downloads Gmail messages matching configurable filters via OAuth2, organized by profiles.
6
+
7
+ ## Build & Run
8
+
9
+ ```bash
10
+ uv sync # Install dependencies
11
+ gmail-streamer run <profile> # Download messages
12
+ gmail-streamer --profile-dir /path run <profile> # Custom profile dir
13
+ gmail-streamer profiles list # List available profiles
14
+ gmail-streamer profiles init <name> # Scaffold new profile
15
+ gmail-streamer profiles show <name> # Show profile config
16
+ ```
17
+
18
+ ## Architecture
19
+
20
+ - `src/gmail_streamer/cli.py` — Click CLI entry point (group with `run` and `profiles` subcommands)
21
+ - `src/gmail_streamer/paths.py` — Profile directory resolution and discovery
22
+ - `src/gmail_streamer/config.py` — Loads and validates `config.yaml` into a `ProfileConfig` dataclass
23
+ - `src/gmail_streamer/auth.py` — OAuth2 flow using google-auth-oauthlib, token caching
24
+ - `src/gmail_streamer/gmail_client.py` — Gmail API wrapper: search, fetch raw messages, fetch attachments
25
+ - `src/gmail_streamer/storage.py` — Saves `.eml` files and attachment files to disk
26
+
27
+ ## Profile Resolution
28
+
29
+ Profiles directory is resolved in this order:
30
+ 1. `--profile-dir` flag or `GMAIL_STREAMER_PROFILE_DIR` env var
31
+ 2. `./profiles/` in current working directory (if it exists)
32
+ 3. `~/.gmail-streamer/profiles/` (fallback)
33
+
34
+ The `profile` argument to `run` can be a name (looked up in the profiles dir) or a path to an existing directory.
35
+
36
+ ## Profile Structure
37
+
38
+ Each profile lives in its own directory with:
39
+ - `config.yaml` — filter query, target directory, mode (full/attachments_only)
40
+ - `credentials.json` — user-provided OAuth client credentials
41
+ - `token.json` — auto-generated after first OAuth flow
42
+
43
+ ## Key Conventions
44
+
45
+ - Python 3.12+, uses hatchling build backend with uv
46
+ - No tests yet
47
+ - Sensitive files (token.json, credentials.json) are gitignored
48
+ - README.md must be kept up to date with any significant project changes
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tiago Silva
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: gmail-streamer
3
+ Version: 0.2.2
4
+ Summary: CLI tool to download Gmail messages matching configurable filters via OAuth2
5
+ Project-URL: Homepage, https://github.com/tsilva/gmail-streamer
6
+ Project-URL: Repository, https://github.com/tsilva/gmail-streamer
7
+ Project-URL: Issues, https://github.com/tsilva/gmail-streamer/issues
8
+ Author: Tiago Silva
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: attachments,backup,cli,download,email,gmail,google,oauth2
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Communications :: Email
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: click
23
+ Requires-Dist: google-api-python-client
24
+ Requires-Dist: google-auth-httplib2
25
+ Requires-Dist: google-auth-oauthlib
26
+ Requires-Dist: pyyaml
27
+ Description-Content-Type: text/markdown
28
+
29
+ <div align="center">
30
+ <img src="logo.png" alt="gmail-streamer" width="512"/>
31
+
32
+ [![Python](https://img.shields.io/badge/Python-3.12+-blue.svg)](https://python.org)
33
+ [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
34
+
35
+ **📧 Download Gmail messages matching your filters to local files 📥**
36
+
37
+ </div>
38
+
39
+ ## ✨ Features
40
+
41
+ - **🗂️ Profile-based configuration** — run multiple independent download profiles, each with its own filters, credentials, and output directory
42
+ - **🔐 OAuth2 authentication** — secure Google sign-in with automatic token caching
43
+ - **📧 Full message download** — save complete `.eml` files for archival
44
+ - **📎 Attachments-only mode** — grab just the attachments, skip the rest
45
+ - **🧠 Incremental downloads** — remembers what's already been downloaded, no duplicates across runs
46
+ - **🔍 Gmail search filters** — use any Gmail search query (`from:`, `has:attachment`, `after:`, label filters, etc.)
47
+ - **🏠 Works from anywhere** — install globally with `uv` and run from any directory
48
+
49
+ ## 🚀 Quick Start
50
+
51
+ ### 1. Install
52
+
53
+ ```bash
54
+ git clone https://github.com/tsilva/gmail-streamer.git
55
+ cd gmail-streamer
56
+ uv tool install . --force --no-cache
57
+ ```
58
+
59
+ ### 2. Create a profile
60
+
61
+ ```bash
62
+ gmail-streamer profiles init my-profile
63
+ ```
64
+
65
+ This creates `~/.gmail-streamer/profiles/my-profile/` with a template `config.yaml`.
66
+
67
+ ### 3. Add your Gmail OAuth credentials
68
+
69
+ Place your `credentials.json` (from [Google Cloud Console](https://console.cloud.google.com/apis/credentials)) into the profile directory.
70
+
71
+ ### 4. Configure the profile
72
+
73
+ Edit `~/.gmail-streamer/profiles/my-profile/config.yaml`:
74
+
75
+ ```yaml
76
+ filter: "from:example@gmail.com has:attachment"
77
+ target_directory: "./downloads"
78
+ mode: "full" # or "attachments_only"
79
+ ```
80
+
81
+ ### 5. Run
82
+
83
+ ```bash
84
+ gmail-streamer run my-profile
85
+ ```
86
+
87
+ On first run, a browser window opens for OAuth authorization. Subsequent runs reuse the cached token.
88
+
89
+ ## 📁 Profile Resolution
90
+
91
+ The profiles directory is resolved in this order:
92
+
93
+ 1. `--profile-dir` flag or `GMAIL_STREAMER_PROFILE_DIR` env var
94
+ 2. `./profiles/` in the current working directory (if it exists)
95
+ 3. `~/.gmail-streamer/profiles/` (default)
96
+
97
+ The `profile` argument can be a **name** (looked up in the profiles directory) or a **path** to an existing directory (backward compatible).
98
+
99
+ ## 🛠️ CLI Reference
100
+
101
+ ```bash
102
+ gmail-streamer run <profile> # Download messages
103
+ gmail-streamer --profile-dir /path run <profile> # Custom profiles directory
104
+ gmail-streamer profiles list # List available profiles
105
+ gmail-streamer profiles init <name> # Scaffold a new profile
106
+ gmail-streamer profiles show <name> # Show profile config
107
+ ```
108
+
109
+ ## ⚙️ Profile Structure
110
+
111
+ Each profile lives in its own directory with:
112
+
113
+ | File | Purpose |
114
+ |------|---------|
115
+ | `config.yaml` | Filter query, target directory, download mode |
116
+ | `credentials.json` | OAuth client credentials (you provide this) |
117
+ | `token.json` | Auto-generated after first OAuth flow |
118
+
119
+ ## 🏗️ Architecture
120
+
121
+ | Module | Responsibility |
122
+ |--------|---------------|
123
+ | `cli.py` | Click CLI entry point (group with `run` and `profiles` subcommands) |
124
+ | `paths.py` | Profile directory resolution and discovery |
125
+ | `config.py` | Loads and validates `config.yaml` into a `ProfileConfig` dataclass |
126
+ | `auth.py` | OAuth2 flow with token caching |
127
+ | `gmail_client.py` | Gmail API wrapper: search, fetch messages, fetch attachments |
128
+ | `storage.py` | Saves `.eml` files and attachments to disk |
129
+
130
+ ## 📄 License
131
+
132
+ [MIT](LICENSE)
@@ -0,0 +1,104 @@
1
+ <div align="center">
2
+ <img src="logo.png" alt="gmail-streamer" width="512"/>
3
+
4
+ [![Python](https://img.shields.io/badge/Python-3.12+-blue.svg)](https://python.org)
5
+ [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
+
7
+ **📧 Download Gmail messages matching your filters to local files 📥**
8
+
9
+ </div>
10
+
11
+ ## ✨ Features
12
+
13
+ - **🗂️ Profile-based configuration** — run multiple independent download profiles, each with its own filters, credentials, and output directory
14
+ - **🔐 OAuth2 authentication** — secure Google sign-in with automatic token caching
15
+ - **📧 Full message download** — save complete `.eml` files for archival
16
+ - **📎 Attachments-only mode** — grab just the attachments, skip the rest
17
+ - **🧠 Incremental downloads** — remembers what's already been downloaded, no duplicates across runs
18
+ - **🔍 Gmail search filters** — use any Gmail search query (`from:`, `has:attachment`, `after:`, label filters, etc.)
19
+ - **🏠 Works from anywhere** — install globally with `uv` and run from any directory
20
+
21
+ ## 🚀 Quick Start
22
+
23
+ ### 1. Install
24
+
25
+ ```bash
26
+ git clone https://github.com/tsilva/gmail-streamer.git
27
+ cd gmail-streamer
28
+ uv tool install . --force --no-cache
29
+ ```
30
+
31
+ ### 2. Create a profile
32
+
33
+ ```bash
34
+ gmail-streamer profiles init my-profile
35
+ ```
36
+
37
+ This creates `~/.gmail-streamer/profiles/my-profile/` with a template `config.yaml`.
38
+
39
+ ### 3. Add your Gmail OAuth credentials
40
+
41
+ Place your `credentials.json` (from [Google Cloud Console](https://console.cloud.google.com/apis/credentials)) into the profile directory.
42
+
43
+ ### 4. Configure the profile
44
+
45
+ Edit `~/.gmail-streamer/profiles/my-profile/config.yaml`:
46
+
47
+ ```yaml
48
+ filter: "from:example@gmail.com has:attachment"
49
+ target_directory: "./downloads"
50
+ mode: "full" # or "attachments_only"
51
+ ```
52
+
53
+ ### 5. Run
54
+
55
+ ```bash
56
+ gmail-streamer run my-profile
57
+ ```
58
+
59
+ On first run, a browser window opens for OAuth authorization. Subsequent runs reuse the cached token.
60
+
61
+ ## 📁 Profile Resolution
62
+
63
+ The profiles directory is resolved in this order:
64
+
65
+ 1. `--profile-dir` flag or `GMAIL_STREAMER_PROFILE_DIR` env var
66
+ 2. `./profiles/` in the current working directory (if it exists)
67
+ 3. `~/.gmail-streamer/profiles/` (default)
68
+
69
+ The `profile` argument can be a **name** (looked up in the profiles directory) or a **path** to an existing directory (backward compatible).
70
+
71
+ ## 🛠️ CLI Reference
72
+
73
+ ```bash
74
+ gmail-streamer run <profile> # Download messages
75
+ gmail-streamer --profile-dir /path run <profile> # Custom profiles directory
76
+ gmail-streamer profiles list # List available profiles
77
+ gmail-streamer profiles init <name> # Scaffold a new profile
78
+ gmail-streamer profiles show <name> # Show profile config
79
+ ```
80
+
81
+ ## ⚙️ Profile Structure
82
+
83
+ Each profile lives in its own directory with:
84
+
85
+ | File | Purpose |
86
+ |------|---------|
87
+ | `config.yaml` | Filter query, target directory, download mode |
88
+ | `credentials.json` | OAuth client credentials (you provide this) |
89
+ | `token.json` | Auto-generated after first OAuth flow |
90
+
91
+ ## 🏗️ Architecture
92
+
93
+ | Module | Responsibility |
94
+ |--------|---------------|
95
+ | `cli.py` | Click CLI entry point (group with `run` and `profiles` subcommands) |
96
+ | `paths.py` | Profile directory resolution and discovery |
97
+ | `config.py` | Loads and validates `config.yaml` into a `ProfileConfig` dataclass |
98
+ | `auth.py` | OAuth2 flow with token caching |
99
+ | `gmail_client.py` | Gmail API wrapper: search, fetch messages, fetch attachments |
100
+ | `storage.py` | Saves `.eml` files and attachments to disk |
101
+
102
+ ## 📄 License
103
+
104
+ [MIT](LICENSE)
@@ -0,0 +1 @@
1
+ GIT_HASH = "4eb78ce"
@@ -0,0 +1,24 @@
1
+ """Hatch build hook to embed git hash at build time."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
7
+
8
+
9
+ class CustomBuildHook(BuildHookInterface):
10
+ def initialize(self, version, build_data):
11
+ try:
12
+ git_hash = subprocess.check_output(
13
+ ["git", "rev-parse", "--short", "HEAD"],
14
+ text=True,
15
+ stderr=subprocess.DEVNULL,
16
+ ).strip()
17
+ except (subprocess.CalledProcessError, FileNotFoundError):
18
+ git_hash = "unknown"
19
+
20
+ build_info_path = Path(self.root) / "src" / "gmail_streamer" / "_build_info.py"
21
+ build_info_path.write_text(f'GIT_HASH = "{git_hash}"\n')
22
+
23
+ # Force include the generated file
24
+ build_data["force_include"][str(build_info_path)] = "gmail_streamer/_build_info.py"
Binary file
@@ -0,0 +1,3 @@
1
+ filter: "from:example@gmail.com has:attachment"
2
+ target_directory: "./downloads"
3
+ mode: "full"
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "gmail-streamer"
3
+ version = "0.2.2"
4
+ description = "CLI tool to download Gmail messages matching configurable filters via OAuth2"
5
+ authors = [{ name = "Tiago Silva" }]
6
+ license = { text = "MIT" }
7
+ readme = "README.md"
8
+ requires-python = ">=3.12"
9
+ keywords = ["gmail", "email", "download", "oauth2", "cli", "google", "attachments", "backup"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "Intended Audience :: End Users/Desktop",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Topic :: Communications :: Email",
18
+ "Environment :: Console",
19
+ "Operating System :: OS Independent",
20
+ ]
21
+ dependencies = [
22
+ "google-api-python-client",
23
+ "google-auth-oauthlib",
24
+ "google-auth-httplib2",
25
+ "click",
26
+ "pyyaml",
27
+ ]
28
+
29
+ [project.scripts]
30
+ gmail-streamer = "gmail_streamer.cli:main"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/tsilva/gmail-streamer"
34
+ Repository = "https://github.com/tsilva/gmail-streamer"
35
+ Issues = "https://github.com/tsilva/gmail-streamer/issues"
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/gmail_streamer"]
43
+
44
+ [tool.hatch.build.hooks.custom]
45
+ path = "hatch_build.py"
@@ -0,0 +1 @@
1
+ __version__ = "0.2.1"
@@ -0,0 +1,33 @@
1
+ from pathlib import Path
2
+
3
+ from google.auth.transport.requests import Request
4
+ from google.oauth2.credentials import Credentials
5
+ from google_auth_oauthlib.flow import InstalledAppFlow
6
+ from googleapiclient.discovery import build
7
+
8
+ SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
9
+
10
+
11
+ def get_gmail_service(profile_dir: Path):
12
+ creds_path = profile_dir / "credentials.json"
13
+ token_path = profile_dir / "token.json"
14
+
15
+ creds = None
16
+ if token_path.exists():
17
+ creds = Credentials.from_authorized_user_file(str(token_path), SCOPES)
18
+
19
+ if not creds or not creds.valid:
20
+ if creds and creds.expired and creds.refresh_token:
21
+ creds.refresh(Request())
22
+ else:
23
+ if not creds_path.exists():
24
+ raise FileNotFoundError(
25
+ f"OAuth credentials not found: {creds_path}\n"
26
+ "Download from Google Cloud Console and place in profile directory."
27
+ )
28
+ flow = InstalledAppFlow.from_client_secrets_file(str(creds_path), SCOPES)
29
+ creds = flow.run_local_server(port=0)
30
+ with open(token_path, "w") as f:
31
+ f.write(creds.to_json())
32
+
33
+ return build("gmail", "v1", credentials=creds)