myl 0.8.13__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.
- {myl-0.8.13 → myl-0.9.0}/PKG-INFO +1 -1
- {myl-0.8.13 → myl-0.9.0}/myl.egg-info/PKG-INFO +1 -1
- myl-0.9.0/myl.py +496 -0
- {myl-0.8.13 → myl-0.9.0}/pyproject.toml +1 -1
- myl-0.8.13/myl.py +0 -325
- {myl-0.8.13 → myl-0.9.0}/.github/dependabot.yml +0 -0
- {myl-0.8.13 → myl-0.9.0}/.github/workflows/lint.yaml +0 -0
- {myl-0.8.13 → myl-0.9.0}/.github/workflows/pypi.yaml +0 -0
- {myl-0.8.13 → myl-0.9.0}/.github/workflows/release.yaml +0 -0
- {myl-0.8.13 → myl-0.9.0}/.gitignore +0 -0
- {myl-0.8.13 → myl-0.9.0}/LICENSE +0 -0
- {myl-0.8.13 → myl-0.9.0}/README.md +0 -0
- {myl-0.8.13 → myl-0.9.0}/myl.egg-info/SOURCES.txt +0 -0
- {myl-0.8.13 → myl-0.9.0}/myl.egg-info/dependency_links.txt +0 -0
- {myl-0.8.13 → myl-0.9.0}/myl.egg-info/entry_points.txt +0 -0
- {myl-0.8.13 → myl-0.9.0}/myl.egg-info/requires.txt +0 -0
- {myl-0.8.13 → myl-0.9.0}/myl.egg-info/top_level.txt +0 -0
- {myl-0.8.13 → myl-0.9.0}/setup.cfg +0 -0
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())
|
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
|
{myl-0.8.13 → myl-0.9.0}/LICENSE
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|