s2k-cli 0.2.0__tar.gz → 0.3.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.
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/PKG-INFO +12 -4
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/README.md +11 -3
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/pyproject.toml +1 -1
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/src/send_2_kindle/config.py +2 -2
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/src/send_2_kindle/constants.py +0 -3
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/src/send_2_kindle/mailer.py +10 -3
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/src/send_2_kindle/main.py +27 -3
- s2k_cli-0.3.0/src/send_2_kindle/providers.py +40 -0
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/src/send_2_kindle/validation.py +21 -4
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/src/send_2_kindle/wizard.py +47 -14
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/LICENSE +0 -0
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/src/send_2_kindle/__init__.py +0 -0
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/src/send_2_kindle/__main__.py +0 -0
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/src/send_2_kindle/doctor.py +0 -0
- {s2k_cli-0.2.0 → s2k_cli-0.3.0}/src/send_2_kindle/errors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: s2k-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Send local files to your Kindle from the command line.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: kindle,send-to-kindle,ebook,email,cli
|
|
@@ -33,7 +33,7 @@ s2k send book.epub paper.pdf notes.docx
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Supported: `.pdf .epub .doc .docx .txt .rtf .html .htm .jpg .jpeg .png .gif .bmp`, up to 50 MB
|
|
36
|
-
each (your email provider may allow less: Gmail
|
|
36
|
+
each (your email provider may allow less: Gmail about 18 MB, iCloud Mail about 14 MB).
|
|
37
37
|
|
|
38
38
|
## Install
|
|
39
39
|
|
|
@@ -48,8 +48,13 @@ pipx install s2k-cli # or: uv tool install s2k-cli
|
|
|
48
48
|
1. **In Amazon** (*Manage Your Content and Devices → Preferences → Personal Document Settings*):
|
|
49
49
|
copy your `@kindle.com` address and add the email you will send from to the *Approved Personal
|
|
50
50
|
Document E-mail List*.
|
|
51
|
-
2. **
|
|
52
|
-
|
|
51
|
+
2. **Create the password your provider requires** — your normal password will not work:
|
|
52
|
+
|
|
53
|
+
| Provider | Password | Server (filled in by `s2k init`) |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Gmail | [App password](https://myaccount.google.com/apppasswords) (needs 2-Step Verification) | `smtp.gmail.com`, 587, STARTTLS |
|
|
56
|
+
| iCloud Mail | App-specific password at [account.apple.com](https://account.apple.com) → Sign-In and Security (needs two-factor authentication); send from your full `@icloud.com` address | `smtp.mail.me.com`, 587, STARTTLS |
|
|
57
|
+
| Other | Your provider's SMTP password | you type it |
|
|
53
58
|
3. Run the wizard in your terminal — it asks for the values, hides the password, saves them to
|
|
54
59
|
`~/.config/s2k/config.env` with private permissions and offers to test the login:
|
|
55
60
|
```bash
|
|
@@ -71,6 +76,9 @@ s2k --version
|
|
|
71
76
|
|
|
72
77
|
Exit codes: `0` success · `1` something failed (see the report) · `2` invalid configuration or usage.
|
|
73
78
|
|
|
79
|
+
Files in iCloud Drive that are not downloaded yet ("Optimize Mac Storage") are downloaded
|
|
80
|
+
automatically when sent, so you need a connection.
|
|
81
|
+
|
|
74
82
|
"Sent" means your email provider accepted the message. If Amazon rejects it (for example, the
|
|
75
83
|
sender is not approved), Amazon emails you.
|
|
76
84
|
|
|
@@ -7,7 +7,7 @@ s2k send book.epub paper.pdf notes.docx
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Supported: `.pdf .epub .doc .docx .txt .rtf .html .htm .jpg .jpeg .png .gif .bmp`, up to 50 MB
|
|
10
|
-
each (your email provider may allow less: Gmail
|
|
10
|
+
each (your email provider may allow less: Gmail about 18 MB, iCloud Mail about 14 MB).
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
@@ -22,8 +22,13 @@ pipx install s2k-cli # or: uv tool install s2k-cli
|
|
|
22
22
|
1. **In Amazon** (*Manage Your Content and Devices → Preferences → Personal Document Settings*):
|
|
23
23
|
copy your `@kindle.com` address and add the email you will send from to the *Approved Personal
|
|
24
24
|
Document E-mail List*.
|
|
25
|
-
2. **
|
|
26
|
-
|
|
25
|
+
2. **Create the password your provider requires** — your normal password will not work:
|
|
26
|
+
|
|
27
|
+
| Provider | Password | Server (filled in by `s2k init`) |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Gmail | [App password](https://myaccount.google.com/apppasswords) (needs 2-Step Verification) | `smtp.gmail.com`, 587, STARTTLS |
|
|
30
|
+
| iCloud Mail | App-specific password at [account.apple.com](https://account.apple.com) → Sign-In and Security (needs two-factor authentication); send from your full `@icloud.com` address | `smtp.mail.me.com`, 587, STARTTLS |
|
|
31
|
+
| Other | Your provider's SMTP password | you type it |
|
|
27
32
|
3. Run the wizard in your terminal — it asks for the values, hides the password, saves them to
|
|
28
33
|
`~/.config/s2k/config.env` with private permissions and offers to test the login:
|
|
29
34
|
```bash
|
|
@@ -45,6 +50,9 @@ s2k --version
|
|
|
45
50
|
|
|
46
51
|
Exit codes: `0` success · `1` something failed (see the report) · `2` invalid configuration or usage.
|
|
47
52
|
|
|
53
|
+
Files in iCloud Drive that are not downloaded yet ("Optimize Mac Storage") are downloaded
|
|
54
|
+
automatically when sent, so you need a connection.
|
|
55
|
+
|
|
48
56
|
"Sent" means your email provider accepted the message. If Amazon rejects it (for example, the
|
|
49
57
|
sender is not approved), Amazon emails you.
|
|
50
58
|
|
|
@@ -8,8 +8,8 @@ from dotenv import dotenv_values
|
|
|
8
8
|
from pydantic import EmailStr, Field, SecretStr, ValidationError, field_validator
|
|
9
9
|
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict
|
|
10
10
|
|
|
11
|
-
from send_2_kindle import constants
|
|
12
11
|
from send_2_kindle.errors import ConfigError
|
|
12
|
+
from send_2_kindle.providers import GMAIL
|
|
13
13
|
|
|
14
14
|
ENV_PREFIX: Final[str] = "S2K_"
|
|
15
15
|
CONFIG_FILE_VARIABLE: Final[str] = "S2K_CONFIG_FILE"
|
|
@@ -49,7 +49,7 @@ class Settings(BaseSettings):
|
|
|
49
49
|
kindle_email: EmailStr
|
|
50
50
|
sender_email: EmailStr
|
|
51
51
|
smtp_password: SecretStr
|
|
52
|
-
smtp_host: SmtpHost =
|
|
52
|
+
smtp_host: SmtpHost = GMAIL.host
|
|
53
53
|
smtp_port: SmtpPort = 587
|
|
54
54
|
smtp_security: SmtpSecurity = "starttls"
|
|
55
55
|
smtp_username: str | None = None
|
|
@@ -14,6 +14,3 @@ ALLOWED_EXTENSIONS: Final[frozenset[str]] = frozenset(
|
|
|
14
14
|
MAX_EMAIL_SIZE_BYTES: Final[int] = 50_000_000
|
|
15
15
|
|
|
16
16
|
SMTP_TIMEOUT_SECONDS: Final[float] = 30.0
|
|
17
|
-
|
|
18
|
-
GMAIL_SMTP_HOST: Final[str] = "smtp.gmail.com"
|
|
19
|
-
GMAIL_APP_PASSWORDS_URL: Final[str] = "https://myaccount.google.com/apppasswords"
|
|
@@ -11,6 +11,8 @@ from typing import Self
|
|
|
11
11
|
from send_2_kindle import constants
|
|
12
12
|
from send_2_kindle.config import Settings
|
|
13
13
|
from send_2_kindle.errors import SendError, SmtpAuthError, SmtpConnectionError
|
|
14
|
+
from send_2_kindle.providers import provider_for_host
|
|
15
|
+
from send_2_kindle.validation import is_not_downloaded
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
def build_message(settings: Settings, path: Path) -> EmailMessage:
|
|
@@ -35,10 +37,11 @@ def _text(value: bytes | str) -> str:
|
|
|
35
37
|
|
|
36
38
|
def _auth_failure_message(settings: Settings) -> str:
|
|
37
39
|
message = f"authentication failed for {settings.login_username} on {settings.smtp_host}"
|
|
38
|
-
|
|
40
|
+
provider = provider_for_host(settings.smtp_host)
|
|
41
|
+
if provider is not None:
|
|
39
42
|
message += (
|
|
40
|
-
".
|
|
41
|
-
f"{
|
|
43
|
+
f". {provider.name} requires an {provider.password_name}, not your account password: "
|
|
44
|
+
f"{provider.password_url}"
|
|
42
45
|
)
|
|
43
46
|
return message
|
|
44
47
|
|
|
@@ -150,6 +153,10 @@ class KindleMailer:
|
|
|
150
153
|
try:
|
|
151
154
|
message = build_message(self._settings, path)
|
|
152
155
|
except OSError as error:
|
|
156
|
+
if is_not_downloaded(path):
|
|
157
|
+
raise SendError(
|
|
158
|
+
f"could not download it from iCloud (are you offline?): {error}"
|
|
159
|
+
) from error
|
|
153
160
|
raise SendError(f"could not read file: {error}") from error
|
|
154
161
|
try:
|
|
155
162
|
self._smtp.send_message(message)
|
|
@@ -6,7 +6,7 @@ from typing import Annotated
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
-
from send_2_kindle import installed_version
|
|
9
|
+
from send_2_kindle import constants, installed_version
|
|
10
10
|
from send_2_kindle.config import load_settings
|
|
11
11
|
from send_2_kindle.doctor import report, run_checks
|
|
12
12
|
from send_2_kindle.errors import (
|
|
@@ -17,7 +17,8 @@ from send_2_kindle.errors import (
|
|
|
17
17
|
SmtpConnectionError,
|
|
18
18
|
)
|
|
19
19
|
from send_2_kindle.mailer import KindleMailer
|
|
20
|
-
from send_2_kindle.
|
|
20
|
+
from send_2_kindle.providers import provider_for_host
|
|
21
|
+
from send_2_kindle.validation import is_not_downloaded, validate_file
|
|
21
22
|
from send_2_kindle.wizard import run_wizard
|
|
22
23
|
|
|
23
24
|
AMAZON_NOTE = (
|
|
@@ -67,16 +68,34 @@ def send(
|
|
|
67
68
|
typer.secho(str(error), fg=typer.colors.RED, err=True)
|
|
68
69
|
raise typer.Exit(code=2) from None
|
|
69
70
|
|
|
71
|
+
provider = provider_for_host(settings.smtp_host)
|
|
72
|
+
max_bytes = min(constants.MAX_EMAIL_SIZE_BYTES, provider.max_file_bytes) if provider else None
|
|
73
|
+
provider_name = provider.name if provider else None
|
|
74
|
+
|
|
70
75
|
outcomes = [Outcome(path) for path in files]
|
|
71
76
|
pending: list[Outcome] = []
|
|
72
77
|
for outcome in outcomes:
|
|
73
78
|
try:
|
|
74
|
-
validate_file(outcome.path)
|
|
79
|
+
validate_file(outcome.path, max_bytes=max_bytes, provider_name=provider_name)
|
|
75
80
|
except FileValidationError as error:
|
|
76
81
|
outcome.reason = str(error)
|
|
77
82
|
else:
|
|
78
83
|
pending.append(outcome)
|
|
79
84
|
|
|
85
|
+
# Download iCloud Drive files before logging in: a long download would leave the SMTP
|
|
86
|
+
# session idle, and a server that drops it would fail every remaining file.
|
|
87
|
+
ready: list[Outcome] = []
|
|
88
|
+
for outcome in pending:
|
|
89
|
+
if is_not_downloaded(outcome.path):
|
|
90
|
+
typer.echo(f'Downloading "{outcome.path.name}" from iCloud…')
|
|
91
|
+
try:
|
|
92
|
+
_fetch_from_icloud(outcome.path)
|
|
93
|
+
except OSError as error:
|
|
94
|
+
outcome.reason = f"could not download it from iCloud (are you offline?): {error}"
|
|
95
|
+
continue
|
|
96
|
+
ready.append(outcome)
|
|
97
|
+
pending = ready
|
|
98
|
+
|
|
80
99
|
if pending:
|
|
81
100
|
try:
|
|
82
101
|
with KindleMailer(settings) as mailer:
|
|
@@ -107,6 +126,11 @@ def init() -> None:
|
|
|
107
126
|
raise typer.Exit(code=run_wizard())
|
|
108
127
|
|
|
109
128
|
|
|
129
|
+
def _fetch_from_icloud(path: Path) -> None:
|
|
130
|
+
"""Reading a not-downloaded iCloud Drive file makes macOS download it."""
|
|
131
|
+
path.read_bytes()
|
|
132
|
+
|
|
133
|
+
|
|
110
134
|
def _print_report(outcomes: list[Outcome]) -> None:
|
|
111
135
|
width = max(len(str(outcome.path)) for outcome in outcomes)
|
|
112
136
|
for outcome in outcomes:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Known email providers: SMTP settings, attachment limits and where to get their passwords."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Provider:
|
|
9
|
+
key: str
|
|
10
|
+
name: str
|
|
11
|
+
host: str
|
|
12
|
+
port: int
|
|
13
|
+
security: str
|
|
14
|
+
# Message limits (Gmail 25 MB, iCloud 20 MB) divided by ~1.37 (base64 4/3 and CRLF line
|
|
15
|
+
# wrapping 78/76), rounded down: 18 MB → ~24.6 MB, 14 MB → ~19.2 MB.
|
|
16
|
+
max_file_bytes: int
|
|
17
|
+
password_name: str
|
|
18
|
+
password_url: str
|
|
19
|
+
aliases: tuple[str, ...] = field(default=())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
GMAIL: Final = Provider(
|
|
23
|
+
"gmail", "Gmail", "smtp.gmail.com", 587, "starttls", 18_000_000,
|
|
24
|
+
"app password", "https://myaccount.google.com/apppasswords",
|
|
25
|
+
) # fmt: skip
|
|
26
|
+
ICLOUD: Final = Provider(
|
|
27
|
+
"icloud", "iCloud", "smtp.mail.me.com", 587, "starttls", 14_000_000,
|
|
28
|
+
"app-specific password", "https://account.apple.com", ("smtp.me.com", "smtp.mac.com"),
|
|
29
|
+
) # fmt: skip
|
|
30
|
+
PROVIDERS: Final[tuple[Provider, ...]] = (GMAIL, ICLOUD)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def provider_for_host(host: str) -> Provider | None:
|
|
34
|
+
"""Return the provider whose SMTP server is ``host`` (case and spaces ignored)."""
|
|
35
|
+
normalized = host.strip().lower()
|
|
36
|
+
return next((p for p in PROVIDERS if normalized == p.host or normalized in p.aliases), None)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def provider_for_key(key: str) -> Provider | None:
|
|
40
|
+
return next((provider for provider in PROVIDERS if provider.key == key), None)
|
|
@@ -3,16 +3,31 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import stat
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from typing import Final
|
|
6
7
|
|
|
7
8
|
from send_2_kindle import constants
|
|
8
9
|
from send_2_kindle.errors import FileValidationError
|
|
9
10
|
|
|
11
|
+
# macOS marks iCloud Drive files whose contents are not on disk yet ("Optimize Mac Storage").
|
|
12
|
+
SF_DATALESS: Final[int] = 0x40000000
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_not_downloaded(path: Path) -> bool:
|
|
16
|
+
"""True for an iCloud Drive file macOS has not downloaded; reading it triggers the download."""
|
|
17
|
+
try:
|
|
18
|
+
flags = getattr(path.stat(), "st_flags", 0)
|
|
19
|
+
except OSError:
|
|
20
|
+
return False
|
|
21
|
+
return bool(flags & SF_DATALESS)
|
|
22
|
+
|
|
10
23
|
|
|
11
24
|
def _megabytes(size: int) -> float:
|
|
12
25
|
return size / 1_000_000
|
|
13
26
|
|
|
14
27
|
|
|
15
|
-
def validate_file(
|
|
28
|
+
def validate_file(
|
|
29
|
+
path: Path, max_bytes: int | None = None, provider_name: str | None = None
|
|
30
|
+
) -> None:
|
|
16
31
|
"""Raise FileValidationError with a user-facing reason if ``path`` cannot be sent."""
|
|
17
32
|
# stat() tells "missing" apart from "can't reach it" (Path.exists() reports both as False).
|
|
18
33
|
try:
|
|
@@ -36,6 +51,8 @@ def validate_file(path: Path) -> None:
|
|
|
36
51
|
raise FileValidationError("file is empty")
|
|
37
52
|
if not os.access(path, os.R_OK):
|
|
38
53
|
raise FileValidationError("file is not readable")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
limit_bytes = constants.MAX_EMAIL_SIZE_BYTES if max_bytes is None else max_bytes
|
|
55
|
+
if size > limit_bytes:
|
|
56
|
+
limit = f"{_megabytes(limit_bytes):g} MB"
|
|
57
|
+
where = f", the limit for {provider_name}" if provider_name else ""
|
|
58
|
+
raise FileValidationError(f"file exceeds {limit}{where} ({_megabytes(size):.1f} MB)")
|
|
@@ -4,12 +4,11 @@ import os
|
|
|
4
4
|
import sys
|
|
5
5
|
import tempfile
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Annotated, Any, Literal
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
|
-
from pydantic import EmailStr, TypeAdapter, ValidationError
|
|
10
|
+
from pydantic import BeforeValidator, EmailStr, TypeAdapter, ValidationError
|
|
11
11
|
|
|
12
|
-
from send_2_kindle import constants
|
|
13
12
|
from send_2_kindle.config import (
|
|
14
13
|
SmtpHost,
|
|
15
14
|
SmtpPort,
|
|
@@ -18,6 +17,7 @@ from send_2_kindle.config import (
|
|
|
18
17
|
read_config_file,
|
|
19
18
|
)
|
|
20
19
|
from send_2_kindle.doctor import report, run_checks
|
|
20
|
+
from send_2_kindle.providers import GMAIL, PROVIDERS, provider_for_host, provider_for_key
|
|
21
21
|
|
|
22
22
|
INTRO = """\
|
|
23
23
|
s2k init: configure Send to Kindle by email.
|
|
@@ -26,14 +26,24 @@ You will need:
|
|
|
26
26
|
- Your Send to Kindle address: Amazon > Manage Your Content and Devices > Preferences >
|
|
27
27
|
Personal Document Settings.
|
|
28
28
|
- The email you send from must be on the "Approved Personal Document E-mail List" (same page).
|
|
29
|
-
|
|
29
|
+
{passwords}
|
|
30
30
|
"""
|
|
31
|
+
PASSWORD_LINES = "\n".join(
|
|
32
|
+
f" - {p.name}: an {p.password_name}, not your account password: {p.password_url}"
|
|
33
|
+
for p in PROVIDERS
|
|
34
|
+
)
|
|
31
35
|
HEADER = "# s2k configuration, written by `s2k init`. Keep it private (chmod 600).\n"
|
|
32
36
|
|
|
33
37
|
_EMAIL: TypeAdapter[Any] = TypeAdapter(EmailStr)
|
|
34
38
|
_HOST: TypeAdapter[Any] = TypeAdapter(SmtpHost)
|
|
35
39
|
_PORT: TypeAdapter[Any] = TypeAdapter(SmtpPort)
|
|
36
40
|
_SECURITY: TypeAdapter[Any] = TypeAdapter(SmtpSecurity)
|
|
41
|
+
_PROVIDER: TypeAdapter[Any] = TypeAdapter(
|
|
42
|
+
Annotated[
|
|
43
|
+
Literal["gmail", "icloud", "other"],
|
|
44
|
+
BeforeValidator(lambda value: value.lower() if isinstance(value, str) else value),
|
|
45
|
+
]
|
|
46
|
+
)
|
|
37
47
|
|
|
38
48
|
|
|
39
49
|
def _is_interactive() -> bool:
|
|
@@ -104,20 +114,43 @@ def _ask_password(has_current: bool) -> str | None:
|
|
|
104
114
|
|
|
105
115
|
|
|
106
116
|
def _collect(current: dict[str, str]) -> dict[str, str]:
|
|
117
|
+
existing_host = current.get("S2K_SMTP_HOST")
|
|
118
|
+
known = provider_for_host(existing_host) if existing_host else GMAIL
|
|
119
|
+
choice = _ask("Email provider (gmail/icloud/other)", _PROVIDER, known.key if known else "other")
|
|
120
|
+
provider = provider_for_key(choice.lower())
|
|
121
|
+
sender_label = "Email you send from"
|
|
122
|
+
if provider is not None and provider.key == "icloud":
|
|
123
|
+
sender_label += " (your iCloud Mail address, e.g. name@icloud.com)"
|
|
107
124
|
values = {
|
|
108
125
|
"S2K_KINDLE_EMAIL": _ask("Send to Kindle address", _EMAIL, current.get("S2K_KINDLE_EMAIL")),
|
|
109
|
-
"S2K_SENDER_EMAIL": _ask(
|
|
110
|
-
"S2K_SMTP_HOST": _ask(
|
|
111
|
-
"SMTP server", _HOST, current.get("S2K_SMTP_HOST", constants.GMAIL_SMTP_HOST)
|
|
112
|
-
),
|
|
113
|
-
"S2K_SMTP_PORT": _ask("SMTP port", _PORT, current.get("S2K_SMTP_PORT", "587")),
|
|
114
|
-
"S2K_SMTP_SECURITY": _ask(
|
|
115
|
-
"Security (starttls/ssl)", _SECURITY, current.get("S2K_SMTP_SECURITY", "starttls")
|
|
116
|
-
),
|
|
126
|
+
"S2K_SENDER_EMAIL": _ask(sender_label, _EMAIL, current.get("S2K_SENDER_EMAIL")),
|
|
117
127
|
}
|
|
128
|
+
if provider is not None:
|
|
129
|
+
# Same provider as before: keep a port/security the user chose (e.g. Gmail on 465/ssl).
|
|
130
|
+
same = existing_host is not None and provider_for_host(existing_host) is provider
|
|
131
|
+
values |= {
|
|
132
|
+
"S2K_SMTP_HOST": provider.host,
|
|
133
|
+
"S2K_SMTP_PORT": current.get("S2K_SMTP_PORT", str(provider.port))
|
|
134
|
+
if same
|
|
135
|
+
else str(provider.port),
|
|
136
|
+
"S2K_SMTP_SECURITY": current.get("S2K_SMTP_SECURITY", provider.security)
|
|
137
|
+
if same
|
|
138
|
+
else provider.security,
|
|
139
|
+
}
|
|
140
|
+
typer.echo(f" {provider.name} needs an {provider.password_name}: {provider.password_url}")
|
|
141
|
+
else:
|
|
142
|
+
values |= {
|
|
143
|
+
"S2K_SMTP_HOST": _ask("SMTP server", _HOST, current.get("S2K_SMTP_HOST", GMAIL.host)),
|
|
144
|
+
"S2K_SMTP_PORT": _ask("SMTP port", _PORT, current.get("S2K_SMTP_PORT", "587")),
|
|
145
|
+
"S2K_SMTP_SECURITY": _ask(
|
|
146
|
+
"Security (starttls/ssl)", _SECURITY, current.get("S2K_SMTP_SECURITY", "starttls")
|
|
147
|
+
),
|
|
148
|
+
}
|
|
118
149
|
password = _ask_password(bool(current.get("S2K_SMTP_PASSWORD")))
|
|
119
150
|
values["S2K_SMTP_PASSWORD"] = password if password is not None else current["S2K_SMTP_PASSWORD"]
|
|
120
|
-
|
|
151
|
+
# A custom login belongs to its server: drop it when the server changes.
|
|
152
|
+
old_host = (existing_host or "").strip().lower()
|
|
153
|
+
if current.get("S2K_SMTP_USERNAME") and values["S2K_SMTP_HOST"].strip().lower() == old_host:
|
|
121
154
|
values["S2K_SMTP_USERNAME"] = current["S2K_SMTP_USERNAME"]
|
|
122
155
|
return values
|
|
123
156
|
|
|
@@ -130,7 +163,7 @@ def run_wizard() -> int:
|
|
|
130
163
|
)
|
|
131
164
|
return 2
|
|
132
165
|
path = config_file_path()
|
|
133
|
-
typer.echo(INTRO.format(
|
|
166
|
+
typer.echo(INTRO.format(passwords=PASSWORD_LINES))
|
|
134
167
|
try:
|
|
135
168
|
values = _collect(_read_existing(path))
|
|
136
169
|
except typer.Abort:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|