sygmail 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .client import Sygmail, SygmailConfig
2
+
3
+ __all__ = ["Sygmail", "SygmailConfig"]
sygmail/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
sygmail/cli.py 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:]))
sygmail/client.py ADDED
@@ -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,10 @@
1
+ sygmail/__init__.py,sha256=eGoC5SwKCqe1cYhH1LuNPogYeeOTd636Wlnt70nRRB8,83
2
+ sygmail/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
3
+ sygmail/cli.py,sha256=rafYsPIfx55WNnaoO6hgNJZpl0jsxNeZJLxZeIk3hbk,4965
4
+ sygmail/client.py,sha256=lLO72zkAIp2MsO-SYh8_HwdOGP7GNEKaLvPBZ8wlqpE,8190
5
+ sygmail-0.1.0.dist-info/licenses/LICENSE,sha256=Oakmmh4GWfLsNpdR0BBBCzmhIf_3rEeIjrB_hvolczk,1076
6
+ sygmail-0.1.0.dist-info/METADATA,sha256=356x36EyQ6pp3vcC57EjlKNKvzd-FH5frZlIsu77xuk,3755
7
+ sygmail-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
8
+ sygmail-0.1.0.dist-info/entry_points.txt,sha256=_6WdaniMLQk9VEx1MnYSNXMIyt7Csce2-PG6mopUGtI,45
9
+ sygmail-0.1.0.dist-info/top_level.txt,sha256=umbnP2FZMQ4N-tovcks9rAP1oN3XW7wu3TyDWq1rw-8,8
10
+ sygmail-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sygmail = sygmail.cli:main
@@ -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.
@@ -0,0 +1 @@
1
+ sygmail