sygmail 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.
- sygmail-0.1.0/LICENSE +21 -0
- sygmail-0.1.0/PKG-INFO +181 -0
- sygmail-0.1.0/README.md +170 -0
- sygmail-0.1.0/pyproject.toml +15 -0
- sygmail-0.1.0/setup.cfg +4 -0
- sygmail-0.1.0/sygmail/__init__.py +3 -0
- sygmail-0.1.0/sygmail/__main__.py +4 -0
- sygmail-0.1.0/sygmail/cli.py +160 -0
- sygmail-0.1.0/sygmail/client.py +247 -0
- sygmail-0.1.0/sygmail.egg-info/PKG-INFO +181 -0
- sygmail-0.1.0/sygmail.egg-info/SOURCES.txt +13 -0
- sygmail-0.1.0/sygmail.egg-info/dependency_links.txt +1 -0
- sygmail-0.1.0/sygmail.egg-info/entry_points.txt +2 -0
- sygmail-0.1.0/sygmail.egg-info/requires.txt +1 -0
- sygmail-0.1.0/sygmail.egg-info/top_level.txt +1 -0
sygmail-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Satoshi Nakabayashi
|
|
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.
|
sygmail-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sygmail
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight Gmail notification wrapper
|
|
5
|
+
Author-email: Satoshi Nakabayashi <3104nkb@gmail.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: yagmail
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# sygmail
|
|
13
|
+
|
|
14
|
+
Lightweight wrapper for sending Gmail notifications with simple defaults and a .env config.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Load settings from `.env` and environment variables
|
|
19
|
+
- Save settings from code (`persist=True`)
|
|
20
|
+
- Defaults for subject/contents with `{script_name}` placeholder
|
|
21
|
+
- Optional auto-attachments from a path
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
pip install sygmail
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Python 3.9+
|
|
32
|
+
- Dependency: `yagmail`
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from sygmail import Sygmail
|
|
38
|
+
|
|
39
|
+
syg = Sygmail()
|
|
40
|
+
syg.configure(
|
|
41
|
+
from_addr="you@gmail.com",
|
|
42
|
+
app_password="app-password",
|
|
43
|
+
persist=True,
|
|
44
|
+
)
|
|
45
|
+
syg.send()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## .env keys
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
SYGMAIL_FROM=you@gmail.com
|
|
52
|
+
SYGMAIL_APP_PASSWORD=app-password
|
|
53
|
+
SYGMAIL_TO=to@example.com
|
|
54
|
+
SYGMAIL_SUBJECT=Process Completed
|
|
55
|
+
SYGMAIL_CONTENTS={script_name} has finished running.
|
|
56
|
+
SYGMAIL_ATTACHMENTS_PATH=./a/
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Defaults
|
|
60
|
+
|
|
61
|
+
- Subject: `Process Completed`
|
|
62
|
+
- Contents: `{script_name} has finished running.`
|
|
63
|
+
|
|
64
|
+
Reset back to defaults:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
syg.reset_subject_contents(persist=True)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Attachments behavior
|
|
71
|
+
|
|
72
|
+
- If `attachments` is provided in `send()`, it is used as-is.
|
|
73
|
+
- If `attachments` is not provided, and `SYGMAIL_ATTACHMENTS_PATH` is set,
|
|
74
|
+
files under that path are attached (files only, no folders).
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
syg.send(attachments=["./a/file.txt"]) # use only this
|
|
80
|
+
syg.send(attachments=[]) # explicitly no attachments
|
|
81
|
+
syg.send() # auto-attach from SYGMAIL_ATTACHMENTS_PATH if set
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## CLI
|
|
85
|
+
|
|
86
|
+
Use `python -m sygmail` for now:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
python -m sygmail send
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Options:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
python -m sygmail send \
|
|
96
|
+
--env .env \
|
|
97
|
+
--from you@gmail.com \
|
|
98
|
+
--to to@example.com \
|
|
99
|
+
--subject "Process Completed" \
|
|
100
|
+
--contents "[sygmail notification]" \
|
|
101
|
+
--attachments ./path/to/file \
|
|
102
|
+
--attachments-path ./path/to/folder/
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- If `--contents` is omitted, CLI uses `[sygmail notification]` without editing `.env`.
|
|
106
|
+
|
|
107
|
+
Common examples:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
python -m sygmail send
|
|
111
|
+
|
|
112
|
+
python -m sygmail send --subject "Job Done" --contents "[sygmail notification]"
|
|
113
|
+
|
|
114
|
+
python -m sygmail send --attachments ./a/a.txt ./a/b.txt
|
|
115
|
+
|
|
116
|
+
python -m sygmail config set --from you@gmail.com --app-password "app-password"
|
|
117
|
+
|
|
118
|
+
python -m sygmail config show
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Config commands:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
python -m sygmail config set \
|
|
125
|
+
--env .env \
|
|
126
|
+
--from you@gmail.com \
|
|
127
|
+
--app-password "app-password" \
|
|
128
|
+
--to to@example.com \
|
|
129
|
+
--subject "Process Completed" \
|
|
130
|
+
--contents "{script_name} has finished running." \
|
|
131
|
+
--attachments-path ./a/
|
|
132
|
+
|
|
133
|
+
python -m sygmail config reset --env .env
|
|
134
|
+
|
|
135
|
+
python -m sygmail config show --env .env
|
|
136
|
+
|
|
137
|
+
python -m sygmail config show --env .env --raw
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## API
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
Sygmail(env_path=".env")
|
|
144
|
+
Sygmail.configure(
|
|
145
|
+
from_addr=None,
|
|
146
|
+
from_=None,
|
|
147
|
+
app_password=None,
|
|
148
|
+
to=None,
|
|
149
|
+
subject=None,
|
|
150
|
+
contents=None,
|
|
151
|
+
attachments_path=None,
|
|
152
|
+
persist=True,
|
|
153
|
+
)
|
|
154
|
+
Sygmail.reset_subject_contents(persist=True)
|
|
155
|
+
Sygmail.send(
|
|
156
|
+
from_addr=None,
|
|
157
|
+
from_=None,
|
|
158
|
+
to=None,
|
|
159
|
+
subject=None,
|
|
160
|
+
contents=None,
|
|
161
|
+
attachments=None,
|
|
162
|
+
attachments_path=None,
|
|
163
|
+
**kwargs,
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Notes
|
|
168
|
+
|
|
169
|
+
- Use a Gmail app password (not your normal password).
|
|
170
|
+
- Settings are stored in `.env` in the current working directory by default.
|
|
171
|
+
- If `to` is omitted, the message is sent to the same address as `from_addr`.
|
|
172
|
+
|
|
173
|
+
## Security
|
|
174
|
+
|
|
175
|
+
- Do not commit `.env` to public repos.
|
|
176
|
+
- Treat app passwords like secrets.
|
|
177
|
+
|
|
178
|
+
## Operations
|
|
179
|
+
|
|
180
|
+
- Prefer `chmod 600 .env` on shared machines.
|
|
181
|
+
- Use `--env` to separate configs per project.
|
sygmail-0.1.0/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# sygmail
|
|
2
|
+
|
|
3
|
+
Lightweight wrapper for sending Gmail notifications with simple defaults and a .env config.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Load settings from `.env` and environment variables
|
|
8
|
+
- Save settings from code (`persist=True`)
|
|
9
|
+
- Defaults for subject/contents with `{script_name}` placeholder
|
|
10
|
+
- Optional auto-attachments from a path
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
pip install sygmail
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Python 3.9+
|
|
21
|
+
- Dependency: `yagmail`
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from sygmail import Sygmail
|
|
27
|
+
|
|
28
|
+
syg = Sygmail()
|
|
29
|
+
syg.configure(
|
|
30
|
+
from_addr="you@gmail.com",
|
|
31
|
+
app_password="app-password",
|
|
32
|
+
persist=True,
|
|
33
|
+
)
|
|
34
|
+
syg.send()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## .env keys
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
SYGMAIL_FROM=you@gmail.com
|
|
41
|
+
SYGMAIL_APP_PASSWORD=app-password
|
|
42
|
+
SYGMAIL_TO=to@example.com
|
|
43
|
+
SYGMAIL_SUBJECT=Process Completed
|
|
44
|
+
SYGMAIL_CONTENTS={script_name} has finished running.
|
|
45
|
+
SYGMAIL_ATTACHMENTS_PATH=./a/
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Defaults
|
|
49
|
+
|
|
50
|
+
- Subject: `Process Completed`
|
|
51
|
+
- Contents: `{script_name} has finished running.`
|
|
52
|
+
|
|
53
|
+
Reset back to defaults:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
syg.reset_subject_contents(persist=True)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Attachments behavior
|
|
60
|
+
|
|
61
|
+
- If `attachments` is provided in `send()`, it is used as-is.
|
|
62
|
+
- If `attachments` is not provided, and `SYGMAIL_ATTACHMENTS_PATH` is set,
|
|
63
|
+
files under that path are attached (files only, no folders).
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
syg.send(attachments=["./a/file.txt"]) # use only this
|
|
69
|
+
syg.send(attachments=[]) # explicitly no attachments
|
|
70
|
+
syg.send() # auto-attach from SYGMAIL_ATTACHMENTS_PATH if set
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## CLI
|
|
74
|
+
|
|
75
|
+
Use `python -m sygmail` for now:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
python -m sygmail send
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
python -m sygmail send \
|
|
85
|
+
--env .env \
|
|
86
|
+
--from you@gmail.com \
|
|
87
|
+
--to to@example.com \
|
|
88
|
+
--subject "Process Completed" \
|
|
89
|
+
--contents "[sygmail notification]" \
|
|
90
|
+
--attachments ./path/to/file \
|
|
91
|
+
--attachments-path ./path/to/folder/
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- If `--contents` is omitted, CLI uses `[sygmail notification]` without editing `.env`.
|
|
95
|
+
|
|
96
|
+
Common examples:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
python -m sygmail send
|
|
100
|
+
|
|
101
|
+
python -m sygmail send --subject "Job Done" --contents "[sygmail notification]"
|
|
102
|
+
|
|
103
|
+
python -m sygmail send --attachments ./a/a.txt ./a/b.txt
|
|
104
|
+
|
|
105
|
+
python -m sygmail config set --from you@gmail.com --app-password "app-password"
|
|
106
|
+
|
|
107
|
+
python -m sygmail config show
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Config commands:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
python -m sygmail config set \
|
|
114
|
+
--env .env \
|
|
115
|
+
--from you@gmail.com \
|
|
116
|
+
--app-password "app-password" \
|
|
117
|
+
--to to@example.com \
|
|
118
|
+
--subject "Process Completed" \
|
|
119
|
+
--contents "{script_name} has finished running." \
|
|
120
|
+
--attachments-path ./a/
|
|
121
|
+
|
|
122
|
+
python -m sygmail config reset --env .env
|
|
123
|
+
|
|
124
|
+
python -m sygmail config show --env .env
|
|
125
|
+
|
|
126
|
+
python -m sygmail config show --env .env --raw
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## API
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
Sygmail(env_path=".env")
|
|
133
|
+
Sygmail.configure(
|
|
134
|
+
from_addr=None,
|
|
135
|
+
from_=None,
|
|
136
|
+
app_password=None,
|
|
137
|
+
to=None,
|
|
138
|
+
subject=None,
|
|
139
|
+
contents=None,
|
|
140
|
+
attachments_path=None,
|
|
141
|
+
persist=True,
|
|
142
|
+
)
|
|
143
|
+
Sygmail.reset_subject_contents(persist=True)
|
|
144
|
+
Sygmail.send(
|
|
145
|
+
from_addr=None,
|
|
146
|
+
from_=None,
|
|
147
|
+
to=None,
|
|
148
|
+
subject=None,
|
|
149
|
+
contents=None,
|
|
150
|
+
attachments=None,
|
|
151
|
+
attachments_path=None,
|
|
152
|
+
**kwargs,
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Notes
|
|
157
|
+
|
|
158
|
+
- Use a Gmail app password (not your normal password).
|
|
159
|
+
- Settings are stored in `.env` in the current working directory by default.
|
|
160
|
+
- If `to` is omitted, the message is sent to the same address as `from_addr`.
|
|
161
|
+
|
|
162
|
+
## Security
|
|
163
|
+
|
|
164
|
+
- Do not commit `.env` to public repos.
|
|
165
|
+
- Treat app passwords like secrets.
|
|
166
|
+
|
|
167
|
+
## Operations
|
|
168
|
+
|
|
169
|
+
- Prefer `chmod 600 .env` on shared machines.
|
|
170
|
+
- Use `--env` to separate configs per project.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sygmail"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight Gmail notification wrapper"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = ["yagmail"]
|
|
12
|
+
authors = [{name = "Satoshi Nakabayashi", email = "3104nkb@gmail.com"}]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
sygmail = "sygmail.cli:main"
|
sygmail-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from .client import Sygmail
|
|
6
|
+
|
|
7
|
+
CLI_DEFAULT_CONTENTS = "[sygmail notification]"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
11
|
+
parser = argparse.ArgumentParser(prog="sygmail", description="Send Gmail notifications.")
|
|
12
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
13
|
+
|
|
14
|
+
send_parser = subparsers.add_parser("send", help="Send a notification email")
|
|
15
|
+
send_parser.add_argument("--env", default=".env", help="Path to .env file")
|
|
16
|
+
send_parser.add_argument("--from", dest="from_addr", help="From address")
|
|
17
|
+
send_parser.add_argument("--to", help="To address")
|
|
18
|
+
send_parser.add_argument("--subject", help="Email subject")
|
|
19
|
+
send_parser.add_argument("--contents", help="Email contents")
|
|
20
|
+
send_parser.add_argument(
|
|
21
|
+
"--attachments",
|
|
22
|
+
nargs="*",
|
|
23
|
+
default=None,
|
|
24
|
+
help="Attachment file paths",
|
|
25
|
+
)
|
|
26
|
+
send_parser.add_argument(
|
|
27
|
+
"--attachments-path",
|
|
28
|
+
dest="attachments_path",
|
|
29
|
+
help="Path to auto-attach files",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
config_parser = subparsers.add_parser("config", help="Manage .env configuration")
|
|
33
|
+
config_subparsers = config_parser.add_subparsers(dest="config_command", required=True)
|
|
34
|
+
|
|
35
|
+
config_set_parser = config_subparsers.add_parser("set", help="Set config values")
|
|
36
|
+
config_set_parser.add_argument("--env", default=".env", help="Path to .env file")
|
|
37
|
+
config_set_parser.add_argument("--from", dest="from_addr", help="From address")
|
|
38
|
+
config_set_parser.add_argument("--app-password", help="Gmail app password")
|
|
39
|
+
config_set_parser.add_argument("--to", help="To address")
|
|
40
|
+
config_set_parser.add_argument("--subject", help="Email subject")
|
|
41
|
+
config_set_parser.add_argument("--contents", help="Email contents")
|
|
42
|
+
config_set_parser.add_argument(
|
|
43
|
+
"--attachments-path",
|
|
44
|
+
dest="attachments_path",
|
|
45
|
+
help="Path to auto-attach files",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
config_reset_parser = config_subparsers.add_parser(
|
|
49
|
+
"reset",
|
|
50
|
+
help="Reset subject/contents to defaults",
|
|
51
|
+
)
|
|
52
|
+
config_reset_parser.add_argument("--env", default=".env", help="Path to .env file")
|
|
53
|
+
|
|
54
|
+
config_show_parser = config_subparsers.add_parser(
|
|
55
|
+
"show",
|
|
56
|
+
help="Show current config values",
|
|
57
|
+
)
|
|
58
|
+
config_show_parser.add_argument("--env", default=".env", help="Path to .env file")
|
|
59
|
+
config_show_parser.add_argument(
|
|
60
|
+
"--raw",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="Show secrets without masking",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return parser
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_send(args: argparse.Namespace) -> int:
|
|
69
|
+
syg = Sygmail(env_path=args.env)
|
|
70
|
+
|
|
71
|
+
contents = args.contents
|
|
72
|
+
if contents is None:
|
|
73
|
+
contents = CLI_DEFAULT_CONTENTS
|
|
74
|
+
|
|
75
|
+
attachments = _normalize_attachments_arg(args.attachments)
|
|
76
|
+
|
|
77
|
+
syg.send(
|
|
78
|
+
from_addr=args.from_addr,
|
|
79
|
+
to=args.to,
|
|
80
|
+
subject=args.subject,
|
|
81
|
+
contents=contents,
|
|
82
|
+
attachments=attachments,
|
|
83
|
+
attachments_path=args.attachments_path,
|
|
84
|
+
)
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _normalize_attachments_arg(raw: Optional[List[str]]) -> Optional[List[str]]:
|
|
89
|
+
if raw is None:
|
|
90
|
+
return None
|
|
91
|
+
if len(raw) == 0:
|
|
92
|
+
return []
|
|
93
|
+
return raw
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _mask_secret(value: Optional[str]) -> str:
|
|
97
|
+
if not value:
|
|
98
|
+
return ""
|
|
99
|
+
if len(value) <= 4:
|
|
100
|
+
return "****"
|
|
101
|
+
return f"****{value[-4:]}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_config_set(args: argparse.Namespace) -> int:
|
|
105
|
+
syg = Sygmail(env_path=args.env)
|
|
106
|
+
syg.configure(
|
|
107
|
+
from_addr=args.from_addr,
|
|
108
|
+
app_password=args.app_password,
|
|
109
|
+
to=args.to,
|
|
110
|
+
subject=args.subject,
|
|
111
|
+
contents=args.contents,
|
|
112
|
+
attachments_path=args.attachments_path,
|
|
113
|
+
persist=True,
|
|
114
|
+
)
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def run_config_reset(args: argparse.Namespace) -> int:
|
|
119
|
+
syg = Sygmail(env_path=args.env)
|
|
120
|
+
syg.reset_subject_contents(persist=True)
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def run_config_show(args: argparse.Namespace) -> int:
|
|
125
|
+
syg = Sygmail(env_path=args.env)
|
|
126
|
+
config = syg.config
|
|
127
|
+
|
|
128
|
+
app_password = config.app_password
|
|
129
|
+
if not args.raw:
|
|
130
|
+
app_password = _mask_secret(app_password)
|
|
131
|
+
|
|
132
|
+
print(f"SYGMAIL_FROM={config.from_addr or ''}")
|
|
133
|
+
print(f"SYGMAIL_APP_PASSWORD={app_password or ''}")
|
|
134
|
+
print(f"SYGMAIL_TO={config.to or ''}")
|
|
135
|
+
print(f"SYGMAIL_SUBJECT={config.subject or ''}")
|
|
136
|
+
print(f"SYGMAIL_CONTENTS={config.contents or ''}")
|
|
137
|
+
print(f"SYGMAIL_ATTACHMENTS_PATH={config.attachments_path or ''}")
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
142
|
+
parser = build_parser()
|
|
143
|
+
args = parser.parse_args(argv)
|
|
144
|
+
|
|
145
|
+
if args.command == "send":
|
|
146
|
+
return run_send(args)
|
|
147
|
+
if args.command == "config":
|
|
148
|
+
if args.config_command == "set":
|
|
149
|
+
return run_config_set(args)
|
|
150
|
+
if args.config_command == "reset":
|
|
151
|
+
return run_config_reset(args)
|
|
152
|
+
if args.config_command == "show":
|
|
153
|
+
return run_config_show(args)
|
|
154
|
+
|
|
155
|
+
parser.print_help()
|
|
156
|
+
return 1
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import warnings
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable, List, Optional
|
|
7
|
+
|
|
8
|
+
import yagmail
|
|
9
|
+
|
|
10
|
+
DEFAULT_SUBJECT = "Process Completed"
|
|
11
|
+
DEFAULT_CONTENTS_TEMPLATE = "{script_name} has finished running."
|
|
12
|
+
|
|
13
|
+
ENV_KEYS = {
|
|
14
|
+
"from_addr": "SYGMAIL_FROM",
|
|
15
|
+
"app_password": "SYGMAIL_APP_PASSWORD",
|
|
16
|
+
"to": "SYGMAIL_TO",
|
|
17
|
+
"subject": "SYGMAIL_SUBJECT",
|
|
18
|
+
"contents": "SYGMAIL_CONTENTS",
|
|
19
|
+
"attachments_path": "SYGMAIL_ATTACHMENTS_PATH",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SygmailConfig:
|
|
25
|
+
from_addr: Optional[str] = None
|
|
26
|
+
app_password: Optional[str] = None
|
|
27
|
+
to: Optional[str] = None
|
|
28
|
+
subject: Optional[str] = None
|
|
29
|
+
contents: Optional[str] = None
|
|
30
|
+
attachments_path: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def load(cls, env_path: str = ".env") -> "SygmailConfig":
|
|
34
|
+
file_values = _read_env_file(env_path)
|
|
35
|
+
values = {**file_values}
|
|
36
|
+
for field, key in ENV_KEYS.items():
|
|
37
|
+
env_value = os.environ.get(key)
|
|
38
|
+
if env_value is None:
|
|
39
|
+
env_value = os.environ.get(key.lower())
|
|
40
|
+
if env_value is not None:
|
|
41
|
+
values[field] = env_value
|
|
42
|
+
return cls(
|
|
43
|
+
from_addr=values.get("from_addr"),
|
|
44
|
+
app_password=values.get("app_password"),
|
|
45
|
+
to=values.get("to"),
|
|
46
|
+
subject=values.get("subject"),
|
|
47
|
+
contents=values.get("contents"),
|
|
48
|
+
attachments_path=values.get("attachments_path"),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def save(self, env_path: str = ".env") -> None:
|
|
52
|
+
data = {
|
|
53
|
+
ENV_KEYS["from_addr"]: self.from_addr or "",
|
|
54
|
+
ENV_KEYS["app_password"]: self.app_password or "",
|
|
55
|
+
ENV_KEYS["to"]: self.to or "",
|
|
56
|
+
ENV_KEYS["subject"]: self.subject or DEFAULT_SUBJECT,
|
|
57
|
+
ENV_KEYS["contents"]: self.contents or DEFAULT_CONTENTS_TEMPLATE,
|
|
58
|
+
}
|
|
59
|
+
if self.attachments_path is not None:
|
|
60
|
+
data[ENV_KEYS["attachments_path"]] = self.attachments_path
|
|
61
|
+
_write_env_file(env_path, data)
|
|
62
|
+
|
|
63
|
+
def reset_subject_contents(self) -> None:
|
|
64
|
+
self.subject = DEFAULT_SUBJECT
|
|
65
|
+
self.contents = DEFAULT_CONTENTS_TEMPLATE
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Sygmail:
|
|
69
|
+
def __init__(self, config: Optional[SygmailConfig] = None, env_path: str = ".env") -> None:
|
|
70
|
+
self.env_path = env_path
|
|
71
|
+
self.config = config or SygmailConfig.load(env_path)
|
|
72
|
+
|
|
73
|
+
def configure(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
from_addr: Optional[str] = None,
|
|
77
|
+
from_: Optional[str] = None,
|
|
78
|
+
app_password: Optional[str] = None,
|
|
79
|
+
to: Optional[str] = None,
|
|
80
|
+
subject: Optional[str] = None,
|
|
81
|
+
contents: Optional[str] = None,
|
|
82
|
+
attachments_path: Optional[str] = None,
|
|
83
|
+
persist: bool = True,
|
|
84
|
+
) -> None:
|
|
85
|
+
if from_addr is None and from_ is not None:
|
|
86
|
+
from_addr = from_
|
|
87
|
+
if from_addr is not None:
|
|
88
|
+
self.config.from_addr = from_addr
|
|
89
|
+
if app_password is not None:
|
|
90
|
+
self.config.app_password = app_password
|
|
91
|
+
if to is not None:
|
|
92
|
+
self.config.to = to
|
|
93
|
+
if subject is not None:
|
|
94
|
+
self.config.subject = subject
|
|
95
|
+
if contents is not None:
|
|
96
|
+
self.config.contents = contents
|
|
97
|
+
if attachments_path is not None:
|
|
98
|
+
self.config.attachments_path = attachments_path
|
|
99
|
+
if persist:
|
|
100
|
+
self.config.save(self.env_path)
|
|
101
|
+
|
|
102
|
+
def reset_subject_contents(self, persist: bool = True) -> None:
|
|
103
|
+
self.config.reset_subject_contents()
|
|
104
|
+
if persist:
|
|
105
|
+
self.config.save(self.env_path)
|
|
106
|
+
|
|
107
|
+
def send(
|
|
108
|
+
self,
|
|
109
|
+
*,
|
|
110
|
+
from_addr: Optional[str] = None,
|
|
111
|
+
from_: Optional[str] = None,
|
|
112
|
+
to: Optional[str] = None,
|
|
113
|
+
subject: Optional[str] = None,
|
|
114
|
+
contents: Optional[str] = None,
|
|
115
|
+
attachments: Optional[Iterable[str]] = None,
|
|
116
|
+
attachments_path: Optional[str] = None,
|
|
117
|
+
**kwargs,
|
|
118
|
+
) -> None:
|
|
119
|
+
resolved_from = from_addr or from_ or self.config.from_addr
|
|
120
|
+
app_password = self.config.app_password
|
|
121
|
+
target = to or self.config.to or resolved_from
|
|
122
|
+
|
|
123
|
+
if not resolved_from or not app_password or not target:
|
|
124
|
+
raise ValueError("from_addr and app_password are required")
|
|
125
|
+
|
|
126
|
+
script_name = _get_script_name()
|
|
127
|
+
subject_value = subject or self.config.subject or DEFAULT_SUBJECT
|
|
128
|
+
contents_value = contents or self.config.contents or DEFAULT_CONTENTS_TEMPLATE
|
|
129
|
+
contents_value = _render_contents(contents_value, script_name)
|
|
130
|
+
|
|
131
|
+
attachments_list = _normalize_attachments(attachments)
|
|
132
|
+
path_value = attachments_path or self.config.attachments_path
|
|
133
|
+
if attachments is None and path_value:
|
|
134
|
+
attachments_list.extend(_collect_attachments(path_value))
|
|
135
|
+
|
|
136
|
+
yag = yagmail.SMTP(resolved_from, app_password)
|
|
137
|
+
yag.send(
|
|
138
|
+
to=target,
|
|
139
|
+
subject=subject_value,
|
|
140
|
+
contents=contents_value,
|
|
141
|
+
attachments=attachments_list if attachments_list else None,
|
|
142
|
+
**kwargs,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _get_script_name() -> str:
|
|
147
|
+
raw = sys.argv[0] if sys.argv and sys.argv[0] else "script"
|
|
148
|
+
return os.path.basename(raw)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _render_contents(contents: str, script_name: str) -> str:
|
|
152
|
+
if "{script_name}" in contents:
|
|
153
|
+
try:
|
|
154
|
+
return contents.format(script_name=script_name)
|
|
155
|
+
except Exception:
|
|
156
|
+
return contents
|
|
157
|
+
return contents
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _normalize_attachments(attachments: Optional[Iterable[str]]) -> List[str]:
|
|
161
|
+
if not attachments:
|
|
162
|
+
return []
|
|
163
|
+
if isinstance(attachments, (str, bytes, os.PathLike)):
|
|
164
|
+
existing, missing = _filter_existing_paths([str(attachments)])
|
|
165
|
+
_warn_missing_attachments(missing)
|
|
166
|
+
return existing
|
|
167
|
+
existing, missing = _filter_existing_paths([str(item) for item in attachments])
|
|
168
|
+
_warn_missing_attachments(missing)
|
|
169
|
+
return existing
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _collect_attachments(attachments_path: str) -> List[str]:
|
|
173
|
+
path = Path(attachments_path)
|
|
174
|
+
if not path.exists():
|
|
175
|
+
_warn_missing_attachments([str(path)])
|
|
176
|
+
return []
|
|
177
|
+
if path.is_file():
|
|
178
|
+
existing, missing = _filter_existing_paths([str(path)])
|
|
179
|
+
_warn_missing_attachments(missing)
|
|
180
|
+
return existing
|
|
181
|
+
existing, missing = _filter_existing_paths([str(child) for child in path.iterdir() if child.is_file()])
|
|
182
|
+
_warn_missing_attachments(missing)
|
|
183
|
+
return existing
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _filter_existing_paths(paths: Iterable[str]) -> tuple[List[str], List[str]]:
|
|
187
|
+
existing: List[str] = []
|
|
188
|
+
missing: List[str] = []
|
|
189
|
+
for value in paths:
|
|
190
|
+
try:
|
|
191
|
+
if Path(value).is_file():
|
|
192
|
+
existing.append(value)
|
|
193
|
+
else:
|
|
194
|
+
missing.append(value)
|
|
195
|
+
except OSError:
|
|
196
|
+
missing.append(value)
|
|
197
|
+
return existing, missing
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _warn_missing_attachments(paths: Iterable[str]) -> None:
|
|
201
|
+
missing = [path for path in paths if path]
|
|
202
|
+
if not missing:
|
|
203
|
+
return
|
|
204
|
+
joined = ", ".join(missing)
|
|
205
|
+
warnings.warn(f"missing attachments ignored: {joined}", stacklevel=3)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _read_env_file(env_path: str) -> dict:
|
|
209
|
+
path = Path(env_path)
|
|
210
|
+
if not path.exists():
|
|
211
|
+
return {}
|
|
212
|
+
|
|
213
|
+
values = {}
|
|
214
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
215
|
+
stripped = line.strip()
|
|
216
|
+
if not stripped or stripped.startswith("#"):
|
|
217
|
+
continue
|
|
218
|
+
if "=" not in stripped:
|
|
219
|
+
continue
|
|
220
|
+
key, value = stripped.split("=", 1)
|
|
221
|
+
key = key.strip()
|
|
222
|
+
value = value.strip().strip("\"'")
|
|
223
|
+
if not key:
|
|
224
|
+
continue
|
|
225
|
+
normalized_key = _normalize_env_key(key)
|
|
226
|
+
if normalized_key in ENV_KEYS.values():
|
|
227
|
+
field = _env_key_to_field(normalized_key)
|
|
228
|
+
if field:
|
|
229
|
+
values[field] = value
|
|
230
|
+
return values
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _write_env_file(env_path: str, data: dict) -> None:
|
|
234
|
+
path = Path(env_path)
|
|
235
|
+
lines = [f"{key}={value}" for key, value in data.items()]
|
|
236
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _normalize_env_key(key: str) -> str:
|
|
240
|
+
return key.upper()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _env_key_to_field(env_key: str) -> Optional[str]:
|
|
244
|
+
for field, key in ENV_KEYS.items():
|
|
245
|
+
if key == env_key:
|
|
246
|
+
return field
|
|
247
|
+
return None
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sygmail
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight Gmail notification wrapper
|
|
5
|
+
Author-email: Satoshi Nakabayashi <3104nkb@gmail.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: yagmail
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# sygmail
|
|
13
|
+
|
|
14
|
+
Lightweight wrapper for sending Gmail notifications with simple defaults and a .env config.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Load settings from `.env` and environment variables
|
|
19
|
+
- Save settings from code (`persist=True`)
|
|
20
|
+
- Defaults for subject/contents with `{script_name}` placeholder
|
|
21
|
+
- Optional auto-attachments from a path
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
pip install sygmail
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Python 3.9+
|
|
32
|
+
- Dependency: `yagmail`
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from sygmail import Sygmail
|
|
38
|
+
|
|
39
|
+
syg = Sygmail()
|
|
40
|
+
syg.configure(
|
|
41
|
+
from_addr="you@gmail.com",
|
|
42
|
+
app_password="app-password",
|
|
43
|
+
persist=True,
|
|
44
|
+
)
|
|
45
|
+
syg.send()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## .env keys
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
SYGMAIL_FROM=you@gmail.com
|
|
52
|
+
SYGMAIL_APP_PASSWORD=app-password
|
|
53
|
+
SYGMAIL_TO=to@example.com
|
|
54
|
+
SYGMAIL_SUBJECT=Process Completed
|
|
55
|
+
SYGMAIL_CONTENTS={script_name} has finished running.
|
|
56
|
+
SYGMAIL_ATTACHMENTS_PATH=./a/
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Defaults
|
|
60
|
+
|
|
61
|
+
- Subject: `Process Completed`
|
|
62
|
+
- Contents: `{script_name} has finished running.`
|
|
63
|
+
|
|
64
|
+
Reset back to defaults:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
syg.reset_subject_contents(persist=True)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Attachments behavior
|
|
71
|
+
|
|
72
|
+
- If `attachments` is provided in `send()`, it is used as-is.
|
|
73
|
+
- If `attachments` is not provided, and `SYGMAIL_ATTACHMENTS_PATH` is set,
|
|
74
|
+
files under that path are attached (files only, no folders).
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
syg.send(attachments=["./a/file.txt"]) # use only this
|
|
80
|
+
syg.send(attachments=[]) # explicitly no attachments
|
|
81
|
+
syg.send() # auto-attach from SYGMAIL_ATTACHMENTS_PATH if set
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## CLI
|
|
85
|
+
|
|
86
|
+
Use `python -m sygmail` for now:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
python -m sygmail send
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Options:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
python -m sygmail send \
|
|
96
|
+
--env .env \
|
|
97
|
+
--from you@gmail.com \
|
|
98
|
+
--to to@example.com \
|
|
99
|
+
--subject "Process Completed" \
|
|
100
|
+
--contents "[sygmail notification]" \
|
|
101
|
+
--attachments ./path/to/file \
|
|
102
|
+
--attachments-path ./path/to/folder/
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- If `--contents` is omitted, CLI uses `[sygmail notification]` without editing `.env`.
|
|
106
|
+
|
|
107
|
+
Common examples:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
python -m sygmail send
|
|
111
|
+
|
|
112
|
+
python -m sygmail send --subject "Job Done" --contents "[sygmail notification]"
|
|
113
|
+
|
|
114
|
+
python -m sygmail send --attachments ./a/a.txt ./a/b.txt
|
|
115
|
+
|
|
116
|
+
python -m sygmail config set --from you@gmail.com --app-password "app-password"
|
|
117
|
+
|
|
118
|
+
python -m sygmail config show
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Config commands:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
python -m sygmail config set \
|
|
125
|
+
--env .env \
|
|
126
|
+
--from you@gmail.com \
|
|
127
|
+
--app-password "app-password" \
|
|
128
|
+
--to to@example.com \
|
|
129
|
+
--subject "Process Completed" \
|
|
130
|
+
--contents "{script_name} has finished running." \
|
|
131
|
+
--attachments-path ./a/
|
|
132
|
+
|
|
133
|
+
python -m sygmail config reset --env .env
|
|
134
|
+
|
|
135
|
+
python -m sygmail config show --env .env
|
|
136
|
+
|
|
137
|
+
python -m sygmail config show --env .env --raw
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## API
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
Sygmail(env_path=".env")
|
|
144
|
+
Sygmail.configure(
|
|
145
|
+
from_addr=None,
|
|
146
|
+
from_=None,
|
|
147
|
+
app_password=None,
|
|
148
|
+
to=None,
|
|
149
|
+
subject=None,
|
|
150
|
+
contents=None,
|
|
151
|
+
attachments_path=None,
|
|
152
|
+
persist=True,
|
|
153
|
+
)
|
|
154
|
+
Sygmail.reset_subject_contents(persist=True)
|
|
155
|
+
Sygmail.send(
|
|
156
|
+
from_addr=None,
|
|
157
|
+
from_=None,
|
|
158
|
+
to=None,
|
|
159
|
+
subject=None,
|
|
160
|
+
contents=None,
|
|
161
|
+
attachments=None,
|
|
162
|
+
attachments_path=None,
|
|
163
|
+
**kwargs,
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Notes
|
|
168
|
+
|
|
169
|
+
- Use a Gmail app password (not your normal password).
|
|
170
|
+
- Settings are stored in `.env` in the current working directory by default.
|
|
171
|
+
- If `to` is omitted, the message is sent to the same address as `from_addr`.
|
|
172
|
+
|
|
173
|
+
## Security
|
|
174
|
+
|
|
175
|
+
- Do not commit `.env` to public repos.
|
|
176
|
+
- Treat app passwords like secrets.
|
|
177
|
+
|
|
178
|
+
## Operations
|
|
179
|
+
|
|
180
|
+
- Prefer `chmod 600 .env` on shared machines.
|
|
181
|
+
- Use `--env` to separate configs per project.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
sygmail/__init__.py
|
|
5
|
+
sygmail/__main__.py
|
|
6
|
+
sygmail/cli.py
|
|
7
|
+
sygmail/client.py
|
|
8
|
+
sygmail.egg-info/PKG-INFO
|
|
9
|
+
sygmail.egg-info/SOURCES.txt
|
|
10
|
+
sygmail.egg-info/dependency_links.txt
|
|
11
|
+
sygmail.egg-info/entry_points.txt
|
|
12
|
+
sygmail.egg-info/requires.txt
|
|
13
|
+
sygmail.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
yagmail
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sygmail
|