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