duckmail 0.1.0__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.
duckmail-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DuckMail Contributors
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,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: duckmail
3
+ Version: 0.1.0
4
+ Summary: Lightweight CLI for DuckDuckGo Email Protection
5
+ Author: DuckMail Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourusername/duckmail
8
+ Project-URL: Documentation, https://github.com/yourusername/duckmail/blob/main/README.md
9
+ Project-URL: Repository, https://github.com/yourusername/duckmail
10
+ Project-URL: Issues, https://github.com/yourusername/duckmail/issues
11
+ Keywords: duckduckgo,email,cli,privacy,email-protection
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Communications :: Email
23
+ Classifier: Topic :: Utilities
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: typer>=0.9.0
28
+ Requires-Dist: httpx>=0.24.0
29
+ Requires-Dist: keyring>=24.0.0
30
+ Requires-Dist: rich>=13.0.0
31
+ Requires-Dist: pydantic>=2.0.0
32
+ Requires-Dist: pyperclip>=1.8.0
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
35
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
36
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
37
+ Requires-Dist: pytest-mock>=3.11.0; extra == "dev"
38
+ Requires-Dist: black>=23.0.0; extra == "dev"
39
+ Requires-Dist: ruff>=0.0.270; extra == "dev"
40
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # DuckMail CLI
44
+
45
+ Lightweight CLI for DuckDuckGo Email Protection - Manage your duck email addresses from the terminal.
46
+
47
+ **[English](README.md)** | [Tiếng Việt](README.vi.md)
48
+
49
+ ---
50
+
51
+ ## Quick Start
52
+
53
+ ```bash
54
+ # Install
55
+ pip install -r requirements.txt
56
+ pip install -e .
57
+
58
+ # First time setup: Sign up via browser
59
+ # Visit https://duckduckgo.com/email/ to create account
60
+
61
+ # Login with your credentials
62
+ duckmail auth login
63
+
64
+ # Generate address and copy to clipboard
65
+ duckmail g -c
66
+ ```
67
+
68
+ > **Note:** Account signup must be done via browser at https://duckduckgo.com/email/ due to DuckDuckGo's bot protection. After signing up, use the CLI to login and manage your addresses.
69
+
70
+ ## Features
71
+
72
+ - 🔐 **Secure Authentication** - OTP-based login
73
+ - 📧 **Generate Addresses** - Create duck email addresses
74
+ - 📋 **Copy to Clipboard** - Auto-copy with `-c` flag
75
+ - ⚡ **Fast & Lightweight** - Quick commands with aliases
76
+ - 💾 **Secure Storage** - System keyring integration
77
+ - 🎨 **Beautiful Output** - Rich terminal UI
78
+
79
+ ## Command Aliases
80
+
81
+ - `duckmail auth` → `duckmail a`
82
+ - `duckmail generate` → `duckmail g` or `duckmail gen`
83
+ - `duckmail info` → `duckmail i`
84
+
85
+ ## Common Commands
86
+
87
+ ```bash
88
+ # Authentication
89
+ duckmail auth login # Login with username
90
+ duckmail auth logout # Logout
91
+ duckmail auth status # Check authentication status
92
+
93
+ # Generate
94
+ duckmail g # Generate 1 address
95
+ duckmail g -c # Generate & copy
96
+ duckmail g -n 5 # Generate 5 addresses
97
+
98
+ # Info
99
+ duckmail i # Account info
100
+ duckmail version # Version
101
+ ```
102
+
103
+ ## Documentation
104
+
105
+ 📖 **[Complete Documentation](DOCS.md)** - Full guide with:
106
+ - Installation instructions
107
+ - Usage examples
108
+ - API documentation
109
+ - Troubleshooting
110
+ - Command reference
111
+
112
+ ## Project Structure
113
+
114
+ ```
115
+ duckmail/
116
+ ├── duckmail/ # Main package
117
+ │ ├── api/ # API clients
118
+ │ ├── config/ # Configuration
119
+ │ ├── utils/ # Utilities
120
+ │ └── cli.py # CLI interface
121
+ ├── DOCS.md # Complete documentation
122
+ ├── PLAN.md # Project plan
123
+ ├── README.md # This file
124
+ └── requirements.txt # Dependencies
125
+ ```
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ # Setup
131
+ python -m venv venv
132
+ .\venv\Scripts\Activate.ps1
133
+ pip install -r requirements.txt
134
+ pip install -e .
135
+
136
+ # Run
137
+ duckmail --help
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT License - See [LICENSE](LICENSE) file
143
+
144
+ ## Disclaimer
145
+
146
+ Unofficial tool, not affiliated with DuckDuckGo. Use at your own risk.
147
+
148
+ ---
149
+
150
+ **Made with ❤️ for the DuckDuckGo community** 🦆
@@ -0,0 +1,108 @@
1
+ # DuckMail CLI
2
+
3
+ Lightweight CLI for DuckDuckGo Email Protection - Manage your duck email addresses from the terminal.
4
+
5
+ **[English](README.md)** | [Tiếng Việt](README.vi.md)
6
+
7
+ ---
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ # Install
13
+ pip install -r requirements.txt
14
+ pip install -e .
15
+
16
+ # First time setup: Sign up via browser
17
+ # Visit https://duckduckgo.com/email/ to create account
18
+
19
+ # Login with your credentials
20
+ duckmail auth login
21
+
22
+ # Generate address and copy to clipboard
23
+ duckmail g -c
24
+ ```
25
+
26
+ > **Note:** Account signup must be done via browser at https://duckduckgo.com/email/ due to DuckDuckGo's bot protection. After signing up, use the CLI to login and manage your addresses.
27
+
28
+ ## Features
29
+
30
+ - 🔐 **Secure Authentication** - OTP-based login
31
+ - 📧 **Generate Addresses** - Create duck email addresses
32
+ - 📋 **Copy to Clipboard** - Auto-copy with `-c` flag
33
+ - ⚡ **Fast & Lightweight** - Quick commands with aliases
34
+ - 💾 **Secure Storage** - System keyring integration
35
+ - 🎨 **Beautiful Output** - Rich terminal UI
36
+
37
+ ## Command Aliases
38
+
39
+ - `duckmail auth` → `duckmail a`
40
+ - `duckmail generate` → `duckmail g` or `duckmail gen`
41
+ - `duckmail info` → `duckmail i`
42
+
43
+ ## Common Commands
44
+
45
+ ```bash
46
+ # Authentication
47
+ duckmail auth login # Login with username
48
+ duckmail auth logout # Logout
49
+ duckmail auth status # Check authentication status
50
+
51
+ # Generate
52
+ duckmail g # Generate 1 address
53
+ duckmail g -c # Generate & copy
54
+ duckmail g -n 5 # Generate 5 addresses
55
+
56
+ # Info
57
+ duckmail i # Account info
58
+ duckmail version # Version
59
+ ```
60
+
61
+ ## Documentation
62
+
63
+ 📖 **[Complete Documentation](DOCS.md)** - Full guide with:
64
+ - Installation instructions
65
+ - Usage examples
66
+ - API documentation
67
+ - Troubleshooting
68
+ - Command reference
69
+
70
+ ## Project Structure
71
+
72
+ ```
73
+ duckmail/
74
+ ├── duckmail/ # Main package
75
+ │ ├── api/ # API clients
76
+ │ ├── config/ # Configuration
77
+ │ ├── utils/ # Utilities
78
+ │ └── cli.py # CLI interface
79
+ ├── DOCS.md # Complete documentation
80
+ ├── PLAN.md # Project plan
81
+ ├── README.md # This file
82
+ └── requirements.txt # Dependencies
83
+ ```
84
+
85
+ ## Development
86
+
87
+ ```bash
88
+ # Setup
89
+ python -m venv venv
90
+ .\venv\Scripts\Activate.ps1
91
+ pip install -r requirements.txt
92
+ pip install -e .
93
+
94
+ # Run
95
+ duckmail --help
96
+ ```
97
+
98
+ ## License
99
+
100
+ MIT License - See [LICENSE](LICENSE) file
101
+
102
+ ## Disclaimer
103
+
104
+ Unofficial tool, not affiliated with DuckDuckGo. Use at your own risk.
105
+
106
+ ---
107
+
108
+ **Made with ❤️ for the DuckDuckGo community** 🦆
File without changes
@@ -0,0 +1,8 @@
1
+ """Main entry point for DuckMail CLI."""
2
+
3
+ from duckmail.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
7
+
8
+ # Made with Bob
File without changes
@@ -0,0 +1,52 @@
1
+ """Address management API for DuckDuckGo Email Protection."""
2
+
3
+ from typing import Optional
4
+ from duckmail.api.client import APIClient
5
+
6
+
7
+ class AddressAPI:
8
+ """Address management API wrapper."""
9
+
10
+ def __init__(self, client: Optional[APIClient] = None):
11
+ """Initialize address API.
12
+
13
+ Args:
14
+ client: Optional API client instance
15
+ """
16
+ self.client = client or APIClient()
17
+
18
+ def generate(self) -> str:
19
+ """Generate a new random duck email address.
20
+
21
+ Returns:
22
+ The generated address alias (without @duck.com)
23
+
24
+ Raises:
25
+ Exception: If generation fails
26
+ """
27
+ try:
28
+ response = self.client.post(
29
+ "/email/addresses",
30
+ use_access_token=True,
31
+ headers={"content-length": "0"}
32
+ )
33
+
34
+ address = response.get("address", "")
35
+ return address
36
+ except Exception as e:
37
+ raise Exception(f"Failed to generate address: {str(e)}")
38
+
39
+ def get_full_address(self, alias: str) -> str:
40
+ """Get full email address from alias.
41
+
42
+ Args:
43
+ alias: Address alias
44
+
45
+ Returns:
46
+ Full email address (alias@duck.com)
47
+ """
48
+ if "@" in alias:
49
+ return alias
50
+ return f"{alias}@duck.com"
51
+
52
+ # Made with Bob
@@ -0,0 +1,314 @@
1
+ """Authentication API for DuckDuckGo Email Protection."""
2
+
3
+ from typing import Optional, Dict, Any
4
+ from duckmail.api.client import APIClient
5
+ from duckmail.utils.storage import CredentialStorage
6
+ from duckmail.config.settings import settings
7
+
8
+
9
+ class AuthAPI:
10
+ """Authentication API wrapper."""
11
+
12
+ def __init__(self, client: Optional[APIClient] = None):
13
+ """Initialize authentication API.
14
+
15
+ Args:
16
+ client: Optional API client instance
17
+ """
18
+ self.client = client or APIClient()
19
+
20
+ def request_login(self, username: str) -> bool:
21
+ """Request login OTP to be sent to user's email.
22
+
23
+ Args:
24
+ username: DuckDuckGo username
25
+
26
+ Returns:
27
+ True if request was successful
28
+
29
+ Raises:
30
+ Exception: If request fails
31
+ """
32
+ try:
33
+ self.client.get(
34
+ "/auth/loginlink",
35
+ params={"user": username}
36
+ )
37
+ return True
38
+ except Exception as e:
39
+ raise Exception(f"Failed to request login: {str(e)}")
40
+
41
+ def confirm_login(self, username: str, otp: str) -> Dict[str, Any]:
42
+ """Confirm login with OTP and get JWT token.
43
+
44
+ Args:
45
+ username: DuckDuckGo username
46
+ otp: OTP passphrase (e.g., "word1+word2+word3+word4")
47
+
48
+ Returns:
49
+ Dictionary with status, token, and user
50
+
51
+ Raises:
52
+ Exception: If login fails
53
+ """
54
+ try:
55
+ response = self.client.get(
56
+ "/auth/login",
57
+ params={
58
+ "user": username,
59
+ "otp": otp
60
+ }
61
+ )
62
+
63
+ # Save JWT token
64
+ if response.get("token"):
65
+ CredentialStorage.save_jwt_token(response["token"])
66
+ # Save username
67
+ settings.save_user_data(username)
68
+
69
+ return response
70
+ except Exception as e:
71
+ raise Exception(f"Login failed: {str(e)}")
72
+
73
+ def get_dashboard(self) -> Dict[str, Any]:
74
+ """Get dashboard information including access token.
75
+
76
+ Returns:
77
+ Dashboard data with user info and access token
78
+
79
+ Raises:
80
+ Exception: If request fails
81
+ """
82
+ try:
83
+ response = self.client.get(
84
+ "/email/dashboard",
85
+ use_jwt=True
86
+ )
87
+
88
+ # Save access token
89
+ if response.get("user", {}).get("access_token"):
90
+ CredentialStorage.save_access_token(response["user"]["access_token"])
91
+ # Save email if available
92
+ if response.get("user", {}).get("email"):
93
+ user_data = settings.load_user_data()
94
+ if user_data:
95
+ settings.save_user_data(
96
+ user_data.get("username", ""),
97
+ response["user"]["email"]
98
+ )
99
+
100
+ return response
101
+ except Exception as e:
102
+ raise Exception(f"Failed to get dashboard: {str(e)}")
103
+
104
+ def logout(self) -> None:
105
+ """Logout by clearing all stored credentials."""
106
+ CredentialStorage.clear_all()
107
+ settings.clear_user_data()
108
+
109
+ def is_authenticated(self) -> bool:
110
+ """Check if user is authenticated.
111
+
112
+ Returns:
113
+ True if JWT token exists
114
+ """
115
+ return CredentialStorage.has_jwt_token()
116
+
117
+ def has_access_token(self) -> bool:
118
+ """Check if access token exists.
119
+
120
+ Returns:
121
+ True if access token is stored
122
+ """
123
+ return CredentialStorage.has_access_token()
124
+
125
+ def verify_token(self) -> bool:
126
+ """Verify if current JWT token is valid.
127
+
128
+ Returns:
129
+ True if token is valid
130
+ """
131
+ if not self.is_authenticated():
132
+ return False
133
+
134
+ try:
135
+ self.get_dashboard()
136
+ return True
137
+ except:
138
+ return False
139
+
140
+ def request_email_change(self, username: str) -> bool:
141
+ """Request OTP to change forwarding email address.
142
+
143
+ Args:
144
+ username: DuckDuckGo username
145
+
146
+ Returns:
147
+ True if request was successful
148
+
149
+ Raises:
150
+ Exception: If request fails
151
+ """
152
+ try:
153
+ self.client.get(
154
+ "/auth/confirmlink",
155
+ params={
156
+ "action": "change_email_address",
157
+ "user": username
158
+ }
159
+ )
160
+ return True
161
+ except Exception as e:
162
+ raise Exception(f"Failed to request email change: {str(e)}")
163
+
164
+ def confirm_email_change_request(self, username: str, otp: str) -> Dict[str, Any]:
165
+ """Confirm email change request with OTP.
166
+
167
+ Args:
168
+ username: DuckDuckGo username
169
+ otp: OTP passphrase
170
+
171
+ Returns:
172
+ Response with temporary token
173
+
174
+ Raises:
175
+ Exception: If confirmation fails
176
+ """
177
+ try:
178
+ response = self.client.get(
179
+ "/auth/confirm",
180
+ params={
181
+ "action": "change_email_address",
182
+ "user": username,
183
+ "otp": otp
184
+ }
185
+ )
186
+ return response
187
+ except Exception as e:
188
+ raise Exception(f"Failed to confirm email change request: {str(e)}")
189
+
190
+ def validate_email(self, email: str) -> bool:
191
+ """Validate if email address is acceptable.
192
+
193
+ Args:
194
+ email: Email address to validate
195
+
196
+ Returns:
197
+ True if email is valid
198
+
199
+ Raises:
200
+ Exception: If validation fails
201
+ """
202
+ try:
203
+ response = self.client.get(
204
+ "/auth/validate-email-address",
205
+ params={"email": email}
206
+ )
207
+ return response.get("valid", False)
208
+ except Exception as e:
209
+ raise Exception(f"Failed to validate email: {str(e)}")
210
+
211
+ def signup_dryrun(self, username: str, email: str, disable_secure_reply: bool = False) -> Dict[str, Any]:
212
+ """Validate username availability before signup.
213
+
214
+ Args:
215
+ username: Desired username
216
+ email: Email address for forwarding
217
+ disable_secure_reply: Whether to disable secure reply
218
+
219
+ Returns:
220
+ Response with status and user
221
+
222
+ Raises:
223
+ Exception: If validation fails
224
+ """
225
+ try:
226
+ # Prepare form data as application/x-www-form-urlencoded
227
+ form_data = {
228
+ "user": username,
229
+ "email": email,
230
+ "disable_secure_reply": "1" if disable_secure_reply else "0",
231
+ "dry_run": "1"
232
+ }
233
+
234
+ response = self.client.post(
235
+ "/auth/signup",
236
+ data=form_data,
237
+ headers={"content-type": "application/x-www-form-urlencoded"}
238
+ )
239
+
240
+ return response
241
+ except Exception as e:
242
+ # Try to extract error message from response
243
+ error_msg = str(e)
244
+ if hasattr(e, 'response') and e.response is not None:
245
+ try:
246
+ error_data = e.response.json()
247
+ if 'error' in error_data:
248
+ error_msg = error_data['error']
249
+ except:
250
+ pass
251
+ raise Exception(f"Failed to validate username: {error_msg}")
252
+
253
+ def signup(self, username: str, email: str, disable_secure_reply: bool = False) -> Dict[str, Any]:
254
+ """Create new DuckDuckGo Email Protection account.
255
+
256
+ Args:
257
+ username: Desired username
258
+ email: Email address for forwarding
259
+ disable_secure_reply: Whether to disable secure reply
260
+
261
+ Returns:
262
+ Response with status and user
263
+
264
+ Raises:
265
+ Exception: If signup fails
266
+ """
267
+ try:
268
+ # Prepare form data as application/x-www-form-urlencoded
269
+ form_data = {
270
+ "user": username,
271
+ "email": email,
272
+ "disable_secure_reply": "1" if disable_secure_reply else "0"
273
+ }
274
+
275
+ response = self.client.post(
276
+ "/auth/signup",
277
+ data=form_data,
278
+ headers={"content-type": "application/x-www-form-urlencoded"}
279
+ )
280
+
281
+ return response
282
+ except Exception as e:
283
+ raise Exception(f"Signup failed: {str(e)}")
284
+
285
+ def verify_signup(self, username: str, otp: str) -> Dict[str, Any]:
286
+ """Verify email and activate account with OTP.
287
+
288
+ Args:
289
+ username: Username
290
+ otp: OTP passphrase from email
291
+
292
+ Returns:
293
+ Response with JWT token
294
+
295
+ Raises:
296
+ Exception: If verification fails
297
+ """
298
+ try:
299
+ response = self.client.get(
300
+ "/auth/verify",
301
+ params={
302
+ "user": username,
303
+ "otp": otp
304
+ }
305
+ )
306
+
307
+ # Save JWT token
308
+ if response.get("token"):
309
+ CredentialStorage.save_jwt_token(response["token"])
310
+ settings.save_user_data(username)
311
+
312
+ return response
313
+ except Exception as e:
314
+ raise Exception(f"Verification failed: {str(e)}")