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 +21 -0
- duckmail-0.1.0/PKG-INFO +150 -0
- duckmail-0.1.0/README.md +108 -0
- duckmail-0.1.0/duckmail/__init__.py +0 -0
- duckmail-0.1.0/duckmail/__main__.py +8 -0
- duckmail-0.1.0/duckmail/api/__init__.py +0 -0
- duckmail-0.1.0/duckmail/api/addresses.py +52 -0
- duckmail-0.1.0/duckmail/api/auth.py +314 -0
- duckmail-0.1.0/duckmail/api/client.py +194 -0
- duckmail-0.1.0/duckmail/cli.py +342 -0
- duckmail-0.1.0/duckmail/config/__init__.py +0 -0
- duckmail-0.1.0/duckmail/config/settings.py +110 -0
- duckmail-0.1.0/duckmail/models/__init__.py +0 -0
- duckmail-0.1.0/duckmail/utils/__init__.py +0 -0
- duckmail-0.1.0/duckmail/utils/storage.py +106 -0
- duckmail-0.1.0/duckmail.egg-info/PKG-INFO +150 -0
- duckmail-0.1.0/duckmail.egg-info/SOURCES.txt +21 -0
- duckmail-0.1.0/duckmail.egg-info/dependency_links.txt +1 -0
- duckmail-0.1.0/duckmail.egg-info/entry_points.txt +2 -0
- duckmail-0.1.0/duckmail.egg-info/requires.txt +15 -0
- duckmail-0.1.0/duckmail.egg-info/top_level.txt +1 -0
- duckmail-0.1.0/pyproject.toml +87 -0
- duckmail-0.1.0/setup.cfg +4 -0
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.
|
duckmail-0.1.0/PKG-INFO
ADDED
|
@@ -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** 🦆
|
duckmail-0.1.0/README.md
ADDED
|
@@ -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
|
|
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)}")
|