myl 0.8.13__tar.gz → 0.9.1__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.
@@ -3,3 +3,4 @@
3
3
  *.egg-info
4
4
  build/
5
5
  dist/
6
+ result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: myl
3
- Version: 0.8.13
3
+ Version: 0.9.1
4
4
  Summary: Dead simple IMAP CLI client
5
5
  Author-email: Philipp Schmitt <philipp@schmitt.co>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -721,15 +721,23 @@ straightforward way to interact with IMAP servers.
721
721
 
722
722
  To install myl, follow these steps:
723
723
 
724
- ```bash
724
+ ```shell
725
725
  pipx install myl
726
+ # or:
727
+ pip install --user myl
728
+ ```
729
+
730
+ on nix you can do this:
731
+
732
+ ```shell
733
+ nix run github.com:pschmitt/myl
726
734
  ```
727
735
 
728
736
  ## 🛠️ Usage
729
737
 
730
738
  Here's how you can use myl:
731
739
 
732
- ```bash
740
+ ```shell
733
741
  myl --help
734
742
  ```
735
743
 
@@ -737,7 +745,7 @@ This command will display the help information for the `myl` command.
737
745
 
738
746
  Here are some examples of using flags with the `myl` command:
739
747
 
740
- ```bash
748
+ ```shell
741
749
  # Connect to an IMAP server
742
750
  myl --server imap.example.com --port 143 --starttls --username "$username" --password "$password"
743
751
 
@@ -27,15 +27,23 @@ straightforward way to interact with IMAP servers.
27
27
 
28
28
  To install myl, follow these steps:
29
29
 
30
- ```bash
30
+ ```shell
31
31
  pipx install myl
32
+ # or:
33
+ pip install --user myl
34
+ ```
35
+
36
+ on nix you can do this:
37
+
38
+ ```shell
39
+ nix run github.com:pschmitt/myl
32
40
  ```
33
41
 
34
42
  ## 🛠️ Usage
35
43
 
36
44
  Here's how you can use myl:
37
45
 
38
- ```bash
46
+ ```shell
39
47
  myl --help
40
48
  ```
41
49
 
@@ -43,7 +51,7 @@ This command will display the help information for the `myl` command.
43
51
 
44
52
  Here are some examples of using flags with the `myl` command:
45
53
 
46
- ```bash
54
+ ```shell
47
55
  # Connect to an IMAP server
48
56
  myl --server imap.example.com --port 143 --starttls --username "$username" --password "$password"
49
57
 
myl-0.9.1/flake.lock ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "nodes": {
3
+ "nixpkgs": {
4
+ "locked": {
5
+ "lastModified": 1730200266,
6
+ "narHash": "sha256-l253w0XMT8nWHGXuXqyiIC/bMvh1VRszGXgdpQlfhvU=",
7
+ "owner": "NixOS",
8
+ "repo": "nixpkgs",
9
+ "rev": "807e9154dcb16384b1b765ebe9cd2bba2ac287fd",
10
+ "type": "github"
11
+ },
12
+ "original": {
13
+ "owner": "NixOS",
14
+ "ref": "nixos-unstable",
15
+ "repo": "nixpkgs",
16
+ "type": "github"
17
+ }
18
+ },
19
+ "root": {
20
+ "inputs": {
21
+ "nixpkgs": "nixpkgs"
22
+ }
23
+ }
24
+ },
25
+ "root": "root",
26
+ "version": 7
27
+ }
myl-0.9.1/flake.nix ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ description = "flake for myl IMAP CLI client and myl-discovery";
3
+
4
+ inputs = {
5
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
+ };
7
+
8
+ outputs =
9
+ { self, nixpkgs }:
10
+ let
11
+ pkgs = import nixpkgs {
12
+ system = "x86_64-linux";
13
+ };
14
+ in
15
+ {
16
+ packages.x86_64-linux.default = self.packages.x86_64-linux.myl;
17
+ packages.x86_64-linux.myl = pkgs.python3.pkgs.buildPythonApplication {
18
+ pname = "myl";
19
+ version = builtins.readFile ./version.txt;
20
+ pyproject = true;
21
+
22
+ src = ./.;
23
+
24
+ buildInputs = [
25
+ pkgs.python3.pkgs.setuptools
26
+ pkgs.python3.pkgs.setuptools-scm
27
+ ];
28
+
29
+ propagatedBuildInputs = with pkgs.python3.pkgs; [
30
+ html2text
31
+ imap-tools
32
+ self.packages.x86_64-linux.myl-discovery
33
+ rich
34
+ ];
35
+
36
+ meta = {
37
+ description = "Dead simple IMAP CLI client";
38
+ homepage = "https://pypi.org/project/myl/";
39
+ license = pkgs.lib.licenses.gpl3Only;
40
+ maintainers = with pkgs.lib.maintainers; [ pschmitt ];
41
+ mainProgram = "myl";
42
+ };
43
+ };
44
+
45
+ packages.x86_64-linux.myl-discovery = pkgs.python3.pkgs.buildPythonApplication rec {
46
+ pname = "myl-discovery";
47
+ version = "0.6.1";
48
+ pyproject = true;
49
+
50
+ src = pkgs.fetchPypi {
51
+ pname = "myl_discovery";
52
+ inherit version;
53
+ hash = "sha256-5ulMzqd9YovEYCKO/B2nLTEvJC+bW76pJtDu1cNXLII=";
54
+ };
55
+
56
+ buildInputs = [
57
+ pkgs.python3.pkgs.setuptools
58
+ pkgs.python3.pkgs.setuptools-scm
59
+ ];
60
+
61
+ propagatedBuildInputs = with pkgs.python3.pkgs; [
62
+ dnspython
63
+ exchangelib
64
+ requests
65
+ rich
66
+ xmltodict
67
+ ];
68
+
69
+ pythonImportsCheck = [ "myldiscovery" ];
70
+
71
+ meta = {
72
+ description = "Email autodiscovery";
73
+ homepage = "https://pypi.org/project/myl-discovery/";
74
+ license = pkgs.lib.licenses.gpl3Only;
75
+ maintainers = with pkgs.lib.maintainers; [ pschmitt ];
76
+ mainProgram = "myl-discovery";
77
+ };
78
+ };
79
+ };
80
+ }
81
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: myl
3
- Version: 0.8.13
3
+ Version: 0.9.1
4
4
  Summary: Dead simple IMAP CLI client
5
5
  Author-email: Philipp Schmitt <philipp@schmitt.co>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -721,15 +721,23 @@ straightforward way to interact with IMAP servers.
721
721
 
722
722
  To install myl, follow these steps:
723
723
 
724
- ```bash
724
+ ```shell
725
725
  pipx install myl
726
+ # or:
727
+ pip install --user myl
728
+ ```
729
+
730
+ on nix you can do this:
731
+
732
+ ```shell
733
+ nix run github.com:pschmitt/myl
726
734
  ```
727
735
 
728
736
  ## 🛠️ Usage
729
737
 
730
738
  Here's how you can use myl:
731
739
 
732
- ```bash
740
+ ```shell
733
741
  myl --help
734
742
  ```
735
743
 
@@ -737,7 +745,7 @@ This command will display the help information for the `myl` command.
737
745
 
738
746
  Here are some examples of using flags with the `myl` command:
739
747
 
740
- ```bash
748
+ ```shell
741
749
  # Connect to an IMAP server
742
750
  myl --server imap.example.com --port 143 --starttls --username "$username" --password "$password"
743
751
 
@@ -1,6 +1,8 @@
1
1
  .gitignore
2
2
  LICENSE
3
3
  README.md
4
+ flake.lock
5
+ flake.nix
4
6
  myl.py
5
7
  pyproject.toml
6
8
  .github/dependabot.yml
myl-0.9.1/myl.py ADDED
@@ -0,0 +1,508 @@
1
+ #!/usr/bin/env python3
2
+ # coding: utf-8
3
+
4
+ from importlib.metadata import version, PackageNotFoundError
5
+ import argparse
6
+ import logging
7
+ import ssl
8
+ import sys
9
+ from json import dumps as json_dumps
10
+
11
+ import html2text
12
+ from imap_tools.consts import MailMessageFlags
13
+ from imap_tools.mailbox import (
14
+ BaseMailBox,
15
+ MailBox,
16
+ MailBoxTls,
17
+ MailBoxUnencrypted,
18
+ )
19
+ from imap_tools.query import AND
20
+ from myldiscovery import autodiscover
21
+ from rich import print, print_json
22
+ from rich.console import Console
23
+ from rich.logging import RichHandler
24
+ from rich.table import Table
25
+
26
+ try:
27
+ __version__ = version("myl")
28
+ except PackageNotFoundError:
29
+ pass
30
+
31
+ LOGGER = logging.getLogger(__name__)
32
+ IMAP_PORT = 993
33
+ GMAIL_IMAP_SERVER = "imap.gmail.com"
34
+ GMAIL_IMAP_PORT = IMAP_PORT
35
+ GMAIL_SENT_FOLDER = "[Gmail]/Sent Mail"
36
+ # GMAIL_ALL_FOLDER = "[Gmail]/All Mail"
37
+
38
+
39
+ class MissingServerException(Exception):
40
+ pass
41
+
42
+
43
+ def error_msg(msg):
44
+ print(f"[red]{msg}[/red]", file=sys.stderr)
45
+
46
+
47
+ def mail_to_dict(msg, date_format="%Y-%m-%d %H:%M:%S"):
48
+ return {
49
+ "uid": msg.uid,
50
+ "subject": msg.subject,
51
+ "from": msg.from_,
52
+ "to": msg.to,
53
+ "date": msg.date.strftime(date_format),
54
+ "timestamp": str(int(msg.date.timestamp())),
55
+ "unread": mail_is_unread(msg),
56
+ "flags": msg.flags,
57
+ "content": {
58
+ "raw": msg.obj.as_string(),
59
+ "html": msg.html,
60
+ "text": msg.text,
61
+ },
62
+ "attachments": msg.attachments,
63
+ }
64
+
65
+
66
+ def mail_to_json(msg, date_format="%Y-%m-%d %H:%M:%S"):
67
+ return json_dumps(mail_to_dict(msg, date_format))
68
+
69
+
70
+ def mail_is_unread(msg):
71
+ return MailMessageFlags.SEEN not in msg.flags
72
+
73
+
74
+ def parse_args():
75
+ parser = argparse.ArgumentParser()
76
+ subparsers = parser.add_subparsers(
77
+ dest="command", help="Available commands"
78
+ )
79
+ parser.add_argument(
80
+ "-V",
81
+ "--version",
82
+ action="version",
83
+ version=f"%(prog)s {__version__}",
84
+ )
85
+
86
+ # Default command: list all emails
87
+ subparsers.add_parser("list", help="List all emails")
88
+
89
+ # Get/show email command
90
+ get_parser = subparsers.add_parser(
91
+ "get", help="Retrieve a specific email or attachment"
92
+ )
93
+ get_parser.add_argument("MAILID", help="Mail ID to fetch", type=int)
94
+ get_parser.add_argument(
95
+ "ATTACHMENT",
96
+ help="Name of the attachment to fetch",
97
+ nargs="?",
98
+ default=None,
99
+ )
100
+
101
+ # Delete email command
102
+ delete_parser = subparsers.add_parser("delete", help="Delete an email")
103
+ delete_parser.add_argument(
104
+ "MAILIDS", help="Mail ID(s) to delete", type=int, nargs="+"
105
+ )
106
+
107
+ # Mark email as read/unread
108
+ mark_read_parser = subparsers.add_parser(
109
+ "read", help="mark an email as read"
110
+ )
111
+ mark_read_parser.add_argument(
112
+ "MAILIDS", help="Mail ID(s) to mark as read", type=int, nargs="+"
113
+ )
114
+ mark_unread_parser = subparsers.add_parser(
115
+ "unread", help="mark an email as unread"
116
+ )
117
+ mark_unread_parser.add_argument(
118
+ "MAILIDS", help="Mail ID(s) to mark as unread", type=int, nargs="+"
119
+ )
120
+
121
+ # Optional arguments
122
+ parser.add_argument(
123
+ "-d", "--debug", help="Enable debug mode", action="store_true"
124
+ )
125
+
126
+ # IMAP connection settings
127
+ parser.add_argument(
128
+ "-s", "--server", help="IMAP server address", required=False
129
+ )
130
+ parser.add_argument(
131
+ "--google",
132
+ "--gmail",
133
+ help="Use Google IMAP settings (overrides --port, --server etc.)",
134
+ action="store_true",
135
+ default=False,
136
+ )
137
+ parser.add_argument(
138
+ "-a",
139
+ "--auto",
140
+ help="Autodiscovery of the required server and port",
141
+ action="store_true",
142
+ default=True,
143
+ )
144
+ parser.add_argument(
145
+ "-P", "--port", help="IMAP server port", default=IMAP_PORT
146
+ )
147
+ parser.add_argument("--ssl", help="SSL", action="store_true", default=True)
148
+ parser.add_argument(
149
+ "--starttls", help="STARTTLS", action="store_true", default=False
150
+ )
151
+ parser.add_argument(
152
+ "--insecure",
153
+ help="Disable cert validation",
154
+ action="store_true",
155
+ default=False,
156
+ )
157
+
158
+ # Credentials
159
+ parser.add_argument(
160
+ "-u", "--username", help="IMAP username", required=True
161
+ )
162
+ password_group = parser.add_mutually_exclusive_group(required=True)
163
+ password_group.add_argument("-p", "--password", help="IMAP password")
164
+ password_group.add_argument(
165
+ "--password-file",
166
+ help="IMAP password (file path)",
167
+ type=argparse.FileType("r"),
168
+ )
169
+
170
+ # Display preferences
171
+ parser.add_argument(
172
+ "-c",
173
+ "--count",
174
+ help="Number of messages to fetch",
175
+ default=10,
176
+ type=int,
177
+ )
178
+ parser.add_argument(
179
+ "-t", "--no-title", help="Do not show title", action="store_true"
180
+ )
181
+ parser.add_argument(
182
+ "--date-format", help="Date format", default="%H:%M %d/%m/%Y"
183
+ )
184
+
185
+ # IMAP actions
186
+ parser.add_argument(
187
+ "-m",
188
+ "--mark-seen",
189
+ help="Mark seen",
190
+ action="store_true",
191
+ default=False,
192
+ )
193
+
194
+ # Email filtering
195
+ parser.add_argument("-f", "--folder", help="IMAP folder", default="INBOX")
196
+ parser.add_argument(
197
+ "--sent",
198
+ help="Sent email",
199
+ action="store_true",
200
+ )
201
+ parser.add_argument("-S", "--search", help="Search string", default="ALL")
202
+ parser.add_argument(
203
+ "--unread",
204
+ help="Limit to unread emails",
205
+ action="store_true",
206
+ default=False,
207
+ )
208
+
209
+ # Output preferences
210
+ parser.add_argument(
211
+ "-H",
212
+ "--html",
213
+ help="Show HTML email",
214
+ action="store_true",
215
+ default=False,
216
+ )
217
+ parser.add_argument(
218
+ "-j",
219
+ "--json",
220
+ help="JSON output",
221
+ action="store_true",
222
+ default=False,
223
+ )
224
+ parser.add_argument(
225
+ "-r",
226
+ "--raw",
227
+ help="Show the raw email",
228
+ action="store_true",
229
+ default=False,
230
+ )
231
+
232
+ return parser.parse_args()
233
+
234
+
235
+ def mb_connect(console, args) -> BaseMailBox:
236
+ imap_password = args.password or (
237
+ args.password_file and args.password_file.read()
238
+ )
239
+
240
+ if args.google:
241
+ args.server = GMAIL_IMAP_SERVER
242
+ args.port = GMAIL_IMAP_PORT
243
+ args.starttls = False
244
+
245
+ if args.sent or args.folder == "Sent":
246
+ args.folder = GMAIL_SENT_FOLDER
247
+ # elif args.folder == "INBOX":
248
+ # args.folder = GMAIL_ALL_FOLDER
249
+ else:
250
+ if args.auto:
251
+ try:
252
+ settings = autodiscover(
253
+ args.username,
254
+ password=imap_password,
255
+ insecure=args.insecure,
256
+ ).get("imap", {})
257
+ except Exception:
258
+ error_msg("Failed to autodiscover IMAP settings")
259
+ if args.debug:
260
+ console.print_exception(show_locals=True)
261
+ raise
262
+
263
+ LOGGER.debug(f"Discovered settings: {settings})")
264
+ args.server = settings.get("server")
265
+ args.port = settings.get("port", IMAP_PORT)
266
+ args.starttls = settings.get("starttls")
267
+ args.ssl = settings.get("ssl")
268
+
269
+ if args.sent:
270
+ args.folder = "Sent"
271
+
272
+ if not args.server:
273
+ error_msg(
274
+ "No server specified\n"
275
+ "You need to either:\n"
276
+ "- specify a server using --server HOSTNAME\n"
277
+ "- set --google if you are using a Gmail account\n"
278
+ "- use --auto to attempt autodiscovery"
279
+ )
280
+ raise MissingServerException()
281
+
282
+ ssl_context = ssl.create_default_context()
283
+ if args.insecure:
284
+ ssl_context.check_hostname = False
285
+ ssl_context.verify_mode = ssl.CERT_NONE
286
+
287
+ mb_kwargs = {"host": args.server, "port": args.port}
288
+ if args.ssl:
289
+ mb = MailBox
290
+ mb_kwargs["ssl_context"] = ssl_context
291
+ elif args.starttls:
292
+ mb = MailBoxTls
293
+ mb_kwargs["ssl_context"] = ssl_context
294
+ else:
295
+ mb = MailBoxUnencrypted
296
+
297
+ mailbox = mb(**mb_kwargs)
298
+ mailbox.login(args.username, imap_password, args.folder)
299
+ return mailbox
300
+
301
+
302
+ def display_single_mail(
303
+ mailbox: BaseMailBox,
304
+ mail_id: int,
305
+ attachment: str | None = None,
306
+ mark_seen: bool = False,
307
+ raw: bool = False,
308
+ html: bool = False,
309
+ json: bool = False,
310
+ ):
311
+ LOGGER.debug("Fetch mail %s", mail_id)
312
+ msg = next(mailbox.fetch(f"UID {mail_id}", mark_seen=mark_seen))
313
+ LOGGER.debug("Fetched mail %s", msg)
314
+
315
+ if attachment:
316
+ for att in msg.attachments:
317
+ if att.filename == attachment:
318
+ sys.stdout.buffer.write(att.payload)
319
+ return 0
320
+ print(
321
+ f"attachment {attachment} not found",
322
+ file=sys.stderr,
323
+ )
324
+ return 1
325
+
326
+ if html:
327
+ output = msg.text
328
+ if raw:
329
+ output = msg.html
330
+ else:
331
+ output = html2text.html2text(msg.html)
332
+ print(output)
333
+ elif raw:
334
+ print(msg.obj.as_string())
335
+ return 0
336
+ elif json:
337
+ print_json(mail_to_json(msg))
338
+ return 0
339
+ else:
340
+ print(msg.text)
341
+
342
+ for att in msg.attachments:
343
+ print(f"📎 Attachment: {att.filename}", file=sys.stderr)
344
+ return 0
345
+
346
+
347
+ def display_emails(
348
+ mailbox,
349
+ console,
350
+ no_title=False,
351
+ search="ALL",
352
+ unread_only=False,
353
+ count=10,
354
+ mark_seen=False,
355
+ json=False,
356
+ date_format="%H:%M %d/%m/%Y",
357
+ ):
358
+ json_data = []
359
+ table = Table(
360
+ show_header=not no_title,
361
+ header_style="bold",
362
+ expand=True,
363
+ show_lines=False,
364
+ show_edge=False,
365
+ pad_edge=False,
366
+ box=None,
367
+ row_styles=["", "dim"],
368
+ )
369
+ table.add_column("ID", style="red", no_wrap=True)
370
+ table.add_column("Subject", style="green", no_wrap=True, ratio=3)
371
+ table.add_column("From", style="blue", no_wrap=True, ratio=2)
372
+ table.add_column("Date", style="cyan", no_wrap=True)
373
+
374
+ if unread_only:
375
+ search = AND(seen=False)
376
+
377
+ for msg in mailbox.fetch(
378
+ criteria=search,
379
+ reverse=True,
380
+ bulk=True,
381
+ limit=count,
382
+ mark_seen=mark_seen,
383
+ headers_only=False, # required for attachments
384
+ ):
385
+ subj_prefix = "🆕 " if mail_is_unread(msg) else ""
386
+ subj_prefix += "📎 " if len(msg.attachments) > 0 else ""
387
+ subject = (
388
+ msg.subject.replace("\n", "") if msg.subject else "<no-subject>"
389
+ )
390
+ if json:
391
+ json_data.append(mail_to_dict(msg))
392
+ else:
393
+ table.add_row(
394
+ msg.uid if msg.uid else "???",
395
+ f"{subj_prefix}{subject}",
396
+ msg.from_,
397
+ (msg.date.strftime(date_format) if msg.date else "???"),
398
+ )
399
+ if table.row_count >= count:
400
+ break
401
+
402
+ if json:
403
+ print_json(json_dumps(json_data))
404
+ else:
405
+ console.print(table)
406
+ if table.row_count == 0:
407
+ print(
408
+ "[yellow italic]No messages[/yellow italic]",
409
+ file=sys.stderr,
410
+ )
411
+ return 0
412
+
413
+
414
+ def delete_emails(mailbox: BaseMailBox, mail_ids: list):
415
+ LOGGER.warning("Deleting mails %s", mail_ids)
416
+ mailbox.delete([str(x) for x in mail_ids])
417
+ return 0
418
+
419
+
420
+ def set_seen(mailbox: BaseMailBox, mail_ids: list, value=True):
421
+ LOGGER.info(
422
+ "Marking mails as %s: %s", "read" if value else "unread", mail_ids
423
+ )
424
+ mailbox.flag(
425
+ [str(x) for x in mail_ids],
426
+ flag_set=(MailMessageFlags.SEEN),
427
+ value=value,
428
+ )
429
+ return 0
430
+
431
+
432
+ def mark_read(mailbox: BaseMailBox, mail_ids: list):
433
+ return set_seen(mailbox, mail_ids, value=True)
434
+
435
+
436
+ def mark_unread(mailbox: BaseMailBox, mail_ids: list):
437
+ return set_seen(mailbox, mail_ids, value=False)
438
+
439
+
440
+ def main() -> int:
441
+ console = Console()
442
+ args = parse_args()
443
+ logging.basicConfig(
444
+ format="%(message)s",
445
+ handlers=[RichHandler(console=console)],
446
+ level=logging.DEBUG if args.debug else logging.INFO,
447
+ )
448
+ LOGGER.debug(args)
449
+
450
+ try:
451
+ with mb_connect(console, args) as mailbox:
452
+ # inbox display
453
+ if args.command in ["list", None]:
454
+ return display_emails(
455
+ mailbox=mailbox,
456
+ console=console,
457
+ no_title=args.no_title,
458
+ search=args.search,
459
+ unread_only=args.unread,
460
+ count=args.count,
461
+ mark_seen=args.mark_seen,
462
+ json=args.json,
463
+ date_format=args.date_format,
464
+ )
465
+
466
+ # single email
467
+ # FIXME $ myl 219 raises an argparse error
468
+ elif args.command in ["get", "show", "display"]:
469
+ return display_single_mail(
470
+ mailbox=mailbox,
471
+ mail_id=args.MAILID,
472
+ attachment=args.ATTACHMENT,
473
+ mark_seen=args.mark_seen,
474
+ raw=args.raw,
475
+ html=args.html,
476
+ json=args.json,
477
+ )
478
+
479
+ # mark emails as read
480
+ elif args.command in ["read"]:
481
+ return mark_read(
482
+ mailbox=mailbox,
483
+ mail_ids=args.MAILIDS,
484
+ )
485
+
486
+ elif args.command in ["unread"]:
487
+ return mark_unread(
488
+ mailbox=mailbox,
489
+ mail_ids=args.MAILIDS,
490
+ )
491
+
492
+ # delete email
493
+ elif args.command in ["delete", "remove"]:
494
+ return delete_emails(
495
+ mailbox=mailbox,
496
+ mail_ids=args.MAILIDS,
497
+ )
498
+ else:
499
+ error_msg(f"Unknown command: {args.command}")
500
+ return 1
501
+
502
+ except Exception:
503
+ console.print_exception(show_locals=True)
504
+ return 1
505
+
506
+
507
+ if __name__ == "__main__":
508
+ sys.exit(main())
@@ -21,7 +21,11 @@ dependencies = [
21
21
  "rich >= 13.0.0, <14.0.0",
22
22
  "html2text >= 2024.2.26"
23
23
  ]
24
- version = "0.8.13"
24
+ dynamic = ["version"]
25
+
26
+ [tool.setuptools_scm]
27
+ write_to = "version.txt"
28
+ version_file = "_version.py"
25
29
 
26
30
  [project.urls]
27
31
  homepage = "https://github.com/pschmitt/myl"
myl-0.8.13/myl.py DELETED
@@ -1,325 +0,0 @@
1
- #!/usr/bin/env python3
2
- # coding: utf-8
3
-
4
- import argparse
5
- import html2text
6
- import json
7
- import logging
8
- import ssl
9
- import sys
10
-
11
- import imap_tools
12
- from myldiscovery import autodiscover
13
- from rich import print, print_json
14
- from rich.console import Console
15
- from rich.logging import RichHandler
16
- from rich.table import Table
17
-
18
- LOGGER = logging.getLogger(__name__)
19
- IMAP_PORT = 993
20
- GMAIL_IMAP_SERVER = "imap.gmail.com"
21
- GMAIL_IMAP_PORT = IMAP_PORT
22
- GMAIL_SENT_FOLDER = "[Gmail]/Sent Mail"
23
- # GMAIL_ALL_FOLDER = "[Gmail]/All Mail"
24
-
25
-
26
- def error_msg(msg):
27
- print(f"[red]{msg}[/red]", file=sys.stderr)
28
-
29
-
30
- def parse_args():
31
- parser = argparse.ArgumentParser()
32
- parser.add_argument("-d", "--debug", help="Debug", action="store_true")
33
- parser.add_argument(
34
- "-s", "--server", help="IMAP server address", required=False
35
- )
36
- parser.add_argument(
37
- "--google",
38
- "--gmail",
39
- help="Use Google IMAP settings (overrides --port, --server etc.)",
40
- action="store_true",
41
- default=False,
42
- )
43
- parser.add_argument(
44
- "-a",
45
- "--auto",
46
- help="Autodiscovery of the required server and port",
47
- action="store_true",
48
- default=False,
49
- )
50
- parser.add_argument(
51
- "-P", "--port", help="IMAP server port", default=IMAP_PORT
52
- )
53
- parser.add_argument("--ssl", help="SSL", action="store_true", default=True)
54
- parser.add_argument(
55
- "--starttls", help="STARTTLS", action="store_true", default=False
56
- )
57
- parser.add_argument(
58
- "--insecure",
59
- help="Disable cert validation",
60
- action="store_true",
61
- default=False,
62
- )
63
- parser.add_argument(
64
- "-c",
65
- "--count",
66
- help="Number of messages to fetch",
67
- default=10,
68
- type=int,
69
- )
70
- parser.add_argument(
71
- "-m", "--mark-seen", help="Mark seen", action="store_true"
72
- )
73
- parser.add_argument(
74
- "-u", "--username", help="IMAP username", required=True
75
- )
76
- password_group = parser.add_mutually_exclusive_group(required=True)
77
- password_group.add_argument("-p", "--password", help="IMAP password")
78
- password_group.add_argument(
79
- "--password-file",
80
- help="IMAP password (file path)",
81
- type=argparse.FileType("r"),
82
- )
83
- parser.add_argument(
84
- "-t", "--no-title", help="Do not show title", action="store_true"
85
- )
86
- parser.add_argument("-f", "--folder", help="IMAP folder", default="INBOX")
87
- parser.add_argument(
88
- "--sent",
89
- help="Sent email",
90
- action="store_true",
91
- )
92
- parser.add_argument("-S", "--search", help="Search string", default="ALL")
93
- parser.add_argument("-H", "--html", help="Show HTML", action="store_true")
94
- parser.add_argument(
95
- "-j",
96
- "--json",
97
- help="JSON output",
98
- action="store_true",
99
- default=False,
100
- )
101
- parser.add_argument(
102
- "-r",
103
- "--raw",
104
- help="Show the raw email",
105
- action="store_true",
106
- default=False,
107
- )
108
- parser.add_argument("MAILID", help="Mail ID to fetch", nargs="?")
109
- parser.add_argument(
110
- "ATTACHMENT", help="Name of the attachment to fetch", nargs="?"
111
- )
112
-
113
- return parser.parse_args()
114
-
115
-
116
- def main():
117
- console = Console()
118
- args = parse_args()
119
- logging.basicConfig(
120
- format="%(message)s",
121
- handlers=[RichHandler(console=console)],
122
- level=logging.DEBUG if args.debug else logging.INFO,
123
- )
124
- LOGGER.debug(args)
125
-
126
- imap_password = args.password or (
127
- args.password_file and args.password_file.read()
128
- )
129
-
130
- if args.google:
131
- args.server = GMAIL_IMAP_SERVER
132
- args.port = GMAIL_IMAP_PORT
133
- args.starttls = False
134
-
135
- if args.sent or args.folder == "Sent":
136
- args.folder = GMAIL_SENT_FOLDER
137
- # elif args.folder == "INBOX":
138
- # args.folder = GMAIL_ALL_FOLDER
139
- else:
140
- if args.auto:
141
- try:
142
- settings = autodiscover(
143
- args.username,
144
- password=imap_password,
145
- insecure=args.insecure,
146
- ).get("imap")
147
- except Exception:
148
- error_msg("Failed to autodiscover IMAP settings")
149
- if args.debug:
150
- console.print_exception(show_locals=True)
151
- return 1
152
- LOGGER.debug(f"Discovered settings: {settings})")
153
- args.server = settings.get("server")
154
- args.port = settings.get("port", IMAP_PORT)
155
- args.starttls = settings.get("starttls")
156
- args.ssl = settings.get("ssl")
157
-
158
- if args.sent:
159
- args.folder = "Sent"
160
-
161
- if not args.server:
162
- error_msg(
163
- "No server specified\n"
164
- "You need to either:\n"
165
- "- specify a server using --server HOSTNAME\n"
166
- "- set --google if you are using a Gmail account\n"
167
- "- use --auto to attempt autodiscovery"
168
- )
169
- return 2
170
-
171
- json_data = []
172
- table = Table(
173
- show_header=not args.no_title,
174
- header_style="bold",
175
- expand=True,
176
- show_lines=False,
177
- show_edge=False,
178
- pad_edge=False,
179
- box=None,
180
- row_styles=["", "dim"],
181
- )
182
- table.add_column("ID", style="red", no_wrap=True)
183
- table.add_column("Subject", style="green", no_wrap=True, ratio=3)
184
- table.add_column("From", style="blue", no_wrap=True, ratio=2)
185
- table.add_column("Date", style="cyan", no_wrap=True)
186
-
187
- ssl_context = ssl.create_default_context()
188
- if args.insecure:
189
- ssl_context.check_hostname = False
190
- ssl_context.verify_mode = ssl.CERT_NONE
191
-
192
- mb_kwargs = {"host": args.server, "port": args.port}
193
- if args.ssl:
194
- mb = imap_tools.MailBox
195
- mb_kwargs["ssl_context"] = ssl_context
196
- elif args.starttls:
197
- mb = imap_tools.MailBoxTls
198
- mb_kwargs["ssl_context"] = ssl_context
199
- else:
200
- mb = imap_tools.MailBoxUnencrypted
201
-
202
- try:
203
- with mb(**mb_kwargs).login(
204
- args.username, imap_password, args.folder
205
- ) as mailbox:
206
- if args.MAILID:
207
- msg = next(
208
- mailbox.fetch(
209
- f"UID {args.MAILID}", mark_seen=args.mark_seen
210
- )
211
- )
212
- if args.ATTACHMENT:
213
- for att in msg.attachments:
214
- if att.filename == args.ATTACHMENT:
215
- sys.stdout.buffer.write(att.payload)
216
- return 0
217
- print(
218
- f"Attachment {args.ATTACHMENT} not found",
219
- file=sys.stderr,
220
- )
221
- return 1
222
- else:
223
- if args.raw:
224
- print(msg.obj.as_string())
225
- return 0
226
- elif args.json:
227
- print_json(
228
- json.dumps(
229
- {
230
- "uid": msg.uid,
231
- "subject": msg.subject,
232
- "from": msg.from_,
233
- "to": msg.to,
234
- "date": msg.date.strftime(
235
- "%Y-%m-%d %H:%M:%S"
236
- ),
237
- "timestamp": str(
238
- int(msg.date.timestamp())
239
- ),
240
- "content": {
241
- "raw": msg.obj.as_string(),
242
- "html": msg.html,
243
- "text": msg.text,
244
- },
245
- "attachments": msg.attachments,
246
- }
247
- )
248
- )
249
- return 0
250
-
251
- output = msg.text
252
- if args.html:
253
- if args.raw:
254
- output = msg.html
255
- else:
256
- output = html2text.html2text(msg.html)
257
- print(output)
258
- for att in msg.attachments:
259
- print(
260
- f"📎 Attachment: {att.filename}", file=sys.stderr
261
- )
262
- return 0
263
-
264
- for msg in mailbox.fetch(
265
- criteria=args.search,
266
- reverse=True,
267
- bulk=True,
268
- limit=args.count,
269
- mark_seen=args.mark_seen,
270
- headers_only=False, # required for attachments
271
- ):
272
- subj_prefix = "📎 " if len(msg.attachments) > 0 else ""
273
- subject = (
274
- msg.subject.replace("\n", "")
275
- if msg.subject
276
- else "<no-subject>"
277
- )
278
- if args.json:
279
- json_data.append(
280
- {
281
- "uid": msg.uid,
282
- "subject": msg.subject,
283
- "from": msg.from_,
284
- "to": msg.to,
285
- "date": msg.date.strftime("%Y-%m-%d %H:%M:%S"),
286
- "timestamp": str(int(msg.date.timestamp())),
287
- "content": {
288
- "raw": msg.obj.as_string(),
289
- "html": msg.html,
290
- "text": msg.text,
291
- },
292
- "attachments": msg.attachments,
293
- }
294
- )
295
- else:
296
- table.add_row(
297
- msg.uid if msg.uid else "???",
298
- f"{subj_prefix}{subject}",
299
- msg.from_,
300
- (
301
- msg.date.strftime("%H:%M %d/%m/%Y")
302
- if msg.date
303
- else "???"
304
- ),
305
- )
306
- if table.row_count >= args.count:
307
- break
308
-
309
- if args.json:
310
- print_json(json.dumps(json_data))
311
- else:
312
- console.print(table)
313
- if table.row_count == 0:
314
- print(
315
- "[yellow italic]No messages[/yellow italic]",
316
- file=sys.stderr,
317
- )
318
- return 0
319
- except Exception:
320
- console.print_exception(show_locals=True)
321
- return 1
322
-
323
-
324
- if __name__ == "__main__":
325
- sys.exit(main())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes