cs-pop3 20260531__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.
- cs_pop3-20260531/PKG-INFO +366 -0
- cs_pop3-20260531/pyproject.toml +393 -0
- cs_pop3-20260531/src/cs/pop3.py +603 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cs-pop3
|
|
3
|
+
Version: 20260531
|
|
4
|
+
Summary: POP3 stuff, particularly a streaming downloader and a simple command line which runs it.
|
|
5
|
+
Keywords: python3
|
|
6
|
+
Author-email: Cameron Simpson <cs@cskk.id.au>
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: Programming Language :: Python
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Topic :: Communications :: Email :: Post-Office :: POP3
|
|
12
|
+
Classifier: Topic :: Internet
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
18
|
+
Requires-Dist: cs.cmdutils>=20210407.1
|
|
19
|
+
Requires-Dist: cs.fs>=20260526
|
|
20
|
+
Requires-Dist: cs.lex>=20260526
|
|
21
|
+
Requires-Dist: cs.logutils>=20250323
|
|
22
|
+
Requires-Dist: cs.pfx>=20250914
|
|
23
|
+
Requires-Dist: cs.queues>=20260531
|
|
24
|
+
Requires-Dist: cs.resources>=20250915
|
|
25
|
+
Requires-Dist: cs.result>=20210407
|
|
26
|
+
Requires-Dist: cs.threads>=20260531
|
|
27
|
+
Requires-Dist: cs.upd>=20260526
|
|
28
|
+
Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
|
|
29
|
+
Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
|
|
30
|
+
Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
|
|
31
|
+
Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/pop3.py
|
|
32
|
+
|
|
33
|
+
POP3 stuff, particularly a streaming downloader and a simple command line which runs it.
|
|
34
|
+
|
|
35
|
+
*Latest release 20260531*:
|
|
36
|
+
POP3Command.cmd_dl: add -1 (once) option for testing, notice SIGINT (though the RETRs get queued so fast it hardly helps), new --limit=n option to limit the number of messages fetched.
|
|
37
|
+
|
|
38
|
+
I spend some time on a geostationary satellite connection,
|
|
39
|
+
where round trip ping times are over 600ms when things are good.
|
|
40
|
+
|
|
41
|
+
My mail setup involves fetching messages from my inbox
|
|
42
|
+
for local storage in my laptop, usually using POP3.
|
|
43
|
+
The common standalone tools for this are `fetchmail` and `getmail`.
|
|
44
|
+
However, both are very subject to the link latency,
|
|
45
|
+
in that they request a message, collect it, issue a delete, then repeat.
|
|
46
|
+
On a satellite link that incurs a cost of over a second per message,
|
|
47
|
+
making catch up after a period offline a many minutes long exercise in tedium.
|
|
48
|
+
|
|
49
|
+
This module does something I've been meaning to do for literally years:
|
|
50
|
+
a bulk fetch. It issues `RETR`ieves for every message up front as fast as possible.
|
|
51
|
+
A separate thread collects the messages as they are delivered
|
|
52
|
+
and issues `DELE`tes for the saved messages as soon as each is saved.
|
|
53
|
+
|
|
54
|
+
This results in a fetch process which is orders of magnitude faster.
|
|
55
|
+
Even on a low latency link the throughput is much faster;
|
|
56
|
+
on the satellite it is gobsmackingly faster.
|
|
57
|
+
|
|
58
|
+
Short summary:
|
|
59
|
+
* `ConnectionSpec`: A specification for a POP3 connection.
|
|
60
|
+
* `main`: The `pop3` command line mode.
|
|
61
|
+
* `NetrcEntry`: A `namedtuple` representation of a `netrc` entry.
|
|
62
|
+
* `POP3`: Simple POP3 class with support for streaming use.
|
|
63
|
+
* `POP3Command`: Command line implementation for POP3 operations.
|
|
64
|
+
|
|
65
|
+
Module contents:
|
|
66
|
+
- <a name="ConnectionSpec"></a>`class ConnectionSpec(ConnectionSpec)`: A specification for a POP3 connection.
|
|
67
|
+
|
|
68
|
+
*`ConnectionSpec.connect(self)`*:
|
|
69
|
+
Connect according to this `ConnectionSpec`, return the `socket`.
|
|
70
|
+
|
|
71
|
+
*`ConnectionSpec.from_spec(spec)`*:
|
|
72
|
+
Construct an instance from a connection spec string
|
|
73
|
+
of the form [`tcp:`|`ssl:`][*user*`@`]*[tcp_host!]server_hostname*[`:`*port*].
|
|
74
|
+
|
|
75
|
+
The optional prefixes `tcp:` and `ssl:` indicate that the connection
|
|
76
|
+
should be cleartext or SSL/TLS respectively.
|
|
77
|
+
The default is SSL/TLS.
|
|
78
|
+
|
|
79
|
+
*`ConnectionSpec.netrc_entry`*:
|
|
80
|
+
The default `NetrcEntry` for this `ConnectionSpec`.
|
|
81
|
+
|
|
82
|
+
*`ConnectionSpec.password`*:
|
|
83
|
+
The password for this connection, obtained from the `.netrc` file
|
|
84
|
+
via the key *user*`@`*host*`:`*port*.
|
|
85
|
+
- <a name="main"></a>`main(argv=None)`: The `pop3` command line mode.
|
|
86
|
+
- <a name="NetrcEntry"></a>`class NetrcEntry(NetrcEntry)`: A `namedtuple` representation of a `netrc` entry.
|
|
87
|
+
|
|
88
|
+
*`NetrcEntry.by_account(account_name, netrc_hosts=None)`*:
|
|
89
|
+
Look up an entry by the `account` field value.
|
|
90
|
+
|
|
91
|
+
*`NetrcEntry.get(machine, netrc_hosts=None)`*:
|
|
92
|
+
Look up an entry by the `machine` field value.
|
|
93
|
+
- <a name="POP3"></a>`class POP3(cs.resources.MultiOpenMixin)`: Simple POP3 class with support for streaming use.
|
|
94
|
+
|
|
95
|
+
*`POP3.client_auth(self, user, password)`*:
|
|
96
|
+
Perform a client authentication.
|
|
97
|
+
|
|
98
|
+
*`POP3.client_begin(self)`*:
|
|
99
|
+
Read the opening server response.
|
|
100
|
+
|
|
101
|
+
*`POP3.client_bg(self, rq_line, is_multiline=False, notify=None)`*:
|
|
102
|
+
Dispatch a request `rq_line` in the background.
|
|
103
|
+
Return a `Result` to collect the request result.
|
|
104
|
+
|
|
105
|
+
Parameters:
|
|
106
|
+
* `rq_line`: POP3 request text, without any terminating CRLF
|
|
107
|
+
* `is_multiline`: true if a multiline response is expected,
|
|
108
|
+
default `False`
|
|
109
|
+
* `notify`: a optional handler for `Result.notify`,
|
|
110
|
+
applied if not `None`
|
|
111
|
+
|
|
112
|
+
*Note*: DOES NOT flush the send stream.
|
|
113
|
+
Call `self.flush()` when a batch of requests has been submitted,
|
|
114
|
+
before trying to collect the `Result`s.
|
|
115
|
+
|
|
116
|
+
The `Result` will receive `[etc,lines]` on success
|
|
117
|
+
where:
|
|
118
|
+
* `etc` is the trailing portion of an ok response line
|
|
119
|
+
* `lines` is a list of unstuffed text lines from the response
|
|
120
|
+
if `is_multiline` is true, `None` otherwise
|
|
121
|
+
The `Result` gets a list instead of a tuple
|
|
122
|
+
so that a handler may clear it in order to release memory.
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
|
|
126
|
+
R = self.client_bg(f'RETR {msg_n}', is_multiline=True, notify=notify)
|
|
127
|
+
|
|
128
|
+
*`POP3.client_dele_bg(self, msg_n)`*:
|
|
129
|
+
Queue a delete request for message `msg_n`,
|
|
130
|
+
return ` Result` for collection.
|
|
131
|
+
|
|
132
|
+
*`POP3.client_quit_bg(self)`*:
|
|
133
|
+
Queue a QUIT request.
|
|
134
|
+
return ` Result` for collection.
|
|
135
|
+
|
|
136
|
+
*`POP3.client_retr_bg(self, msg_n, notify=None)`*:
|
|
137
|
+
Queue a retrieve request for message `msg_n`,
|
|
138
|
+
return ` Result` for collection.
|
|
139
|
+
|
|
140
|
+
If `notify` is not `None`, apply it to the `Result`.
|
|
141
|
+
|
|
142
|
+
*`POP3.client_uidl(self)`*:
|
|
143
|
+
Return a mapping of message number to message UID string.
|
|
144
|
+
|
|
145
|
+
*`POP3.dl_bg(self, msg_n, maildir, deleRs)`*:
|
|
146
|
+
Download message `msg_n` to Maildir `maildir`.
|
|
147
|
+
Return the `Result` for the `RETR` request.
|
|
148
|
+
|
|
149
|
+
After a successful save,
|
|
150
|
+
queue a `DELE` for the message
|
|
151
|
+
and add its `Result` to `deleRs`.
|
|
152
|
+
|
|
153
|
+
*`POP3.flush(self)`*:
|
|
154
|
+
Flush the send stream.
|
|
155
|
+
|
|
156
|
+
*`POP3.get_multiline(self)`*:
|
|
157
|
+
Generator yielding unstuffed lines from a multiline response.
|
|
158
|
+
|
|
159
|
+
*`POP3.get_ok(self)`*:
|
|
160
|
+
Read server response, require it to be `'OK+'`.
|
|
161
|
+
Returns the `etc` part.
|
|
162
|
+
|
|
163
|
+
*`POP3.get_response(self)`*:
|
|
164
|
+
Read a server response.
|
|
165
|
+
Return `(ok,status,etc)`
|
|
166
|
+
where `ok` is true if `status` is `'+OK'`, false otherwise;
|
|
167
|
+
`status` is the status word
|
|
168
|
+
and `etc` is the following text.
|
|
169
|
+
Return `(None,None,None)` on EOF from the receive stream.
|
|
170
|
+
|
|
171
|
+
*`POP3.readline(self)`*:
|
|
172
|
+
Read a CRLF terminated line from `self.recvf`.
|
|
173
|
+
Return the text preceeding the CRLF.
|
|
174
|
+
Return `None` at EOF.
|
|
175
|
+
|
|
176
|
+
*`POP3.readlines(self)`*:
|
|
177
|
+
Generator yielding lines from `self.recf`.
|
|
178
|
+
|
|
179
|
+
*`POP3.sendline(self, line, do_flush=False)`*:
|
|
180
|
+
Send a line (excluding its terminating CRLF).
|
|
181
|
+
If `do_flush` is true (default `False`)
|
|
182
|
+
also flush the sending stream.
|
|
183
|
+
|
|
184
|
+
*`POP3.startup_shutdown(self)`*:
|
|
185
|
+
Connect to the server and log in.
|
|
186
|
+
- <a name="POP3Command"></a>`class POP3Command(cs.cmdutils.BaseCommand)`: Command line implementation for POP3 operations.
|
|
187
|
+
|
|
188
|
+
Credentials are obtained via the `.netrc` file presently.
|
|
189
|
+
|
|
190
|
+
Connection specifications consist of an optional leading mode prefix
|
|
191
|
+
followed by a netrc(5) account name
|
|
192
|
+
or an explicit connection specification
|
|
193
|
+
from which to derive:
|
|
194
|
+
* `user`: the user name to log in as
|
|
195
|
+
* `tcp_host`: the hostname to which to establish a TCP connection
|
|
196
|
+
* `port`: the TCP port to connect on, default 995 for TLS/SSL or 110 for cleartext
|
|
197
|
+
* `sni_host`: the TLS/SSL SNI server name, which may be different from the `tcp_host`
|
|
198
|
+
|
|
199
|
+
The optional mode prefix is one of:
|
|
200
|
+
* `ssl:`: use TLS/SSL - this is the default
|
|
201
|
+
* `tcp:`: use cleartext - this is useful for ssh port forwards
|
|
202
|
+
to some not-publicly-exposed clear text POP service;
|
|
203
|
+
in particular streaming performs better this way,
|
|
204
|
+
I think because the Python SSL layer does not buffer writes
|
|
205
|
+
|
|
206
|
+
Example connection specifications:
|
|
207
|
+
* `username@mail.example.com`:
|
|
208
|
+
use TLS/SSL to connect to the POP3S service at `mail.example.com`,
|
|
209
|
+
logging in as `username`
|
|
210
|
+
* `mail.example.com`:
|
|
211
|
+
use TLS/SSL to connect to the POP3S service at `mail.example.com`,
|
|
212
|
+
logging in with the same login as the local effective user
|
|
213
|
+
* `tcp:username@localhost:1110`:
|
|
214
|
+
use cleartext to connect to `localhost:1110`,
|
|
215
|
+
typically an ssh port forward to a remote private cleartext POP service,
|
|
216
|
+
logging in as `username`
|
|
217
|
+
* `username@localhost!mail.example.com:1995`:
|
|
218
|
+
use TLS/SSL to connect to `localhost:1995`,
|
|
219
|
+
usually an ssh port forward to a remote private TLS/SSL POP service,
|
|
220
|
+
logging in as `username` and passing `mail.exampl.com`
|
|
221
|
+
as the TLS/SSL server name indication
|
|
222
|
+
(which allows certificate verification to proceed correctly)
|
|
223
|
+
|
|
224
|
+
Note that the specification may also be a `netrc` account name.
|
|
225
|
+
If the specification matches such an account name
|
|
226
|
+
then values are derived from the `netrc` entry.
|
|
227
|
+
The entry's `machine` name becomes the TCP connection specification,
|
|
228
|
+
the entry's `login` provides a default for the username,
|
|
229
|
+
the entry's `account` host part provides the `sni_host`.
|
|
230
|
+
|
|
231
|
+
Example `netrc` entry:
|
|
232
|
+
|
|
233
|
+
machine username@localhost:1110
|
|
234
|
+
account username@mail.example.com
|
|
235
|
+
password ************
|
|
236
|
+
|
|
237
|
+
Such an entry allows you to use the specification `tcp:username@mail.example.com`
|
|
238
|
+
and obtain the remaining detail via the `netrc` entry.Credentials are obtained via the `.netrc` file presently.
|
|
239
|
+
|
|
240
|
+
Connection specifications consist of an optional leading mode prefix
|
|
241
|
+
followed by a netrc(5) account name
|
|
242
|
+
or an explicit connection specification
|
|
243
|
+
from which to derive:
|
|
244
|
+
* `user`: the user name to log in as
|
|
245
|
+
* `tcp_host`: the hostname to which to establish a TCP connection
|
|
246
|
+
* `port`: the TCP port to connect on, default 995 for TLS/SSL or 110 for cleartext
|
|
247
|
+
* `sni_host`: the TLS/SSL SNI server name, which may be different from the `tcp_host`
|
|
248
|
+
|
|
249
|
+
The optional mode prefix is one of:
|
|
250
|
+
* `ssl:`: use TLS/SSL - this is the default
|
|
251
|
+
* `tcp:`: use cleartext - this is useful for ssh port forwards
|
|
252
|
+
to some not-publicly-exposed clear text POP service;
|
|
253
|
+
in particular streaming performs better this way,
|
|
254
|
+
I think because the Python SSL layer does not buffer writes
|
|
255
|
+
|
|
256
|
+
Example connection specifications:
|
|
257
|
+
* `username@mail.example.com`:
|
|
258
|
+
use TLS/SSL to connect to the POP3S service at `mail.example.com`,
|
|
259
|
+
logging in as `username`
|
|
260
|
+
* `mail.example.com`:
|
|
261
|
+
use TLS/SSL to connect to the POP3S service at `mail.example.com`,
|
|
262
|
+
logging in with the same login as the local effective user
|
|
263
|
+
* `tcp:username@localhost:1110`:
|
|
264
|
+
use cleartext to connect to `localhost:1110`,
|
|
265
|
+
typically an ssh port forward to a remote private cleartext POP service,
|
|
266
|
+
logging in as `username`
|
|
267
|
+
* `username@localhost!mail.example.com:1995`:
|
|
268
|
+
use TLS/SSL to connect to `localhost:1995`,
|
|
269
|
+
usually an ssh port forward to a remote private TLS/SSL POP service,
|
|
270
|
+
logging in as `username` and passing `mail.exampl.com`
|
|
271
|
+
as the TLS/SSL server name indication
|
|
272
|
+
(which allows certificate verification to proceed correctly)
|
|
273
|
+
|
|
274
|
+
Note that the specification may also be a `netrc` account name.
|
|
275
|
+
If the specification matches such an account name
|
|
276
|
+
then values are derived from the `netrc` entry.
|
|
277
|
+
The entry's `machine` name becomes the TCP connection specification,
|
|
278
|
+
the entry's `login` provides a default for the username,
|
|
279
|
+
the entry's `account` host part provides the `sni_host`.
|
|
280
|
+
|
|
281
|
+
Example `netrc` entry:
|
|
282
|
+
|
|
283
|
+
machine username@localhost:1110
|
|
284
|
+
account username@mail.example.com
|
|
285
|
+
password ************
|
|
286
|
+
|
|
287
|
+
Such an entry allows you to use the specification `tcp:username@mail.example.com`
|
|
288
|
+
and obtain the remaining detail via the `netrc` entry.
|
|
289
|
+
|
|
290
|
+
Usage summary:
|
|
291
|
+
|
|
292
|
+
Usage: pop3 [common-options...] subcommand [options...]
|
|
293
|
+
Command line implementation for POP3 operations.
|
|
294
|
+
Subcommands:
|
|
295
|
+
dl [common-options...] [{ssl,tcp}:]{netrc_account|[user@]host[!sni_name][:port]} maildir
|
|
296
|
+
Collect messages from a POP3 server and deliver to a Maildir.
|
|
297
|
+
Options:
|
|
298
|
+
-1 Download only 1 message.
|
|
299
|
+
--limit limit Limit the number of messages fetched.
|
|
300
|
+
help [common-options...] [-l] [-s] [subcommand-names...]
|
|
301
|
+
Print help for subcommands.
|
|
302
|
+
This outputs the full help for the named subcommands,
|
|
303
|
+
or the short help for all subcommands if no names are specified.
|
|
304
|
+
Options:
|
|
305
|
+
-l Long listing.
|
|
306
|
+
-r Recurse into subcommands.
|
|
307
|
+
-s Short listing.
|
|
308
|
+
info [common-options...] [field-names...]
|
|
309
|
+
Recite general information.
|
|
310
|
+
Explicit field names may be provided to override the default listing.
|
|
311
|
+
repl [common-options...]
|
|
312
|
+
Run a REPL (Read Evaluate Print Loop), an interactive Python prompt.
|
|
313
|
+
Options:
|
|
314
|
+
--banner banner Banner.
|
|
315
|
+
shell [common-options...]
|
|
316
|
+
Run a command prompt via cmd.Cmd using this command's subcommands.
|
|
317
|
+
|
|
318
|
+
*`POP3Command.cmd_dl(self, argv)`*:
|
|
319
|
+
Usage: {cmd} [{{ssl,tcp}}:]{{netrc_account|[user@]host[!sni_name][:port]}} maildir
|
|
320
|
+
Collect messages from a POP3 server and deliver to a Maildir.
|
|
321
|
+
Options:
|
|
322
|
+
-1 Download only 1 message.
|
|
323
|
+
--limit limit Limit the number of messages fetched.
|
|
324
|
+
|
|
325
|
+
# Release Log
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
*Release 20260531*:
|
|
330
|
+
POP3Command.cmd_dl: add -1 (once) option for testing, notice SIGINT (though the RETRs get queued so fast it hardly helps), new --limit=n option to limit the number of messages fetched.
|
|
331
|
+
|
|
332
|
+
*Release 20240316*:
|
|
333
|
+
Fixed release upload artifacts.
|
|
334
|
+
|
|
335
|
+
*Release 20240201.1*:
|
|
336
|
+
Another test release, nothing new.
|
|
337
|
+
|
|
338
|
+
*Release 20240201*:
|
|
339
|
+
Test release with better DISTINFO.
|
|
340
|
+
|
|
341
|
+
*Release 20221221*:
|
|
342
|
+
Fix stray %s in format string, modernise MultiOpenMixin startup/shutdown, catch ConnectionRefusedError and report succintly.
|
|
343
|
+
|
|
344
|
+
*Release 20220918*:
|
|
345
|
+
* Emit an error instead of stack trace for messages which cannot be saved (and do not delete).
|
|
346
|
+
* POP3Command.cmd_dl: new -n (no action) option.
|
|
347
|
+
|
|
348
|
+
*Release 20220606*:
|
|
349
|
+
Minor updates.
|
|
350
|
+
|
|
351
|
+
*Release 20220312*:
|
|
352
|
+
Make POP3Command.cmd_dl an instance method (static methods broke with the latest cs.cmdutils release).
|
|
353
|
+
|
|
354
|
+
*Release 20211208*:
|
|
355
|
+
* POP3.startup: do not start the worker queue until authenticated.
|
|
356
|
+
* POP3.get_response: return (None,None,None) at EOF.
|
|
357
|
+
* POP3.shutdown: catch exceptions from client QUIT.
|
|
358
|
+
|
|
359
|
+
*Release 20210407.2*:
|
|
360
|
+
Provide "pop3" console_script.
|
|
361
|
+
|
|
362
|
+
*Release 20210407.1*:
|
|
363
|
+
Bump for cs.cmdutils minor bugfix, also fix a few docstring typos.
|
|
364
|
+
|
|
365
|
+
*Release 20210407*:
|
|
366
|
+
Initial PyPI release.
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cs-pop3"
|
|
3
|
+
description = "POP3 stuff, particularly a streaming downloader and a simple command line which runs it."
|
|
4
|
+
authors = [
|
|
5
|
+
{ name = "Cameron Simpson", email = "cs@cskk.id.au" },
|
|
6
|
+
]
|
|
7
|
+
keywords = [
|
|
8
|
+
"python3",
|
|
9
|
+
]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"cs.cmdutils>=20210407.1",
|
|
12
|
+
"cs.fs>=20260526",
|
|
13
|
+
"cs.lex>=20260526",
|
|
14
|
+
"cs.logutils>=20250323",
|
|
15
|
+
"cs.pfx>=20250914",
|
|
16
|
+
"cs.queues>=20260531",
|
|
17
|
+
"cs.resources>=20250915",
|
|
18
|
+
"cs.result>=20210407",
|
|
19
|
+
"cs.threads>=20260531",
|
|
20
|
+
"cs.upd>=20260526",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Programming Language :: Python",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Environment :: Console",
|
|
26
|
+
"Topic :: Communications :: Email :: Post-Office :: POP3",
|
|
27
|
+
"Topic :: Internet",
|
|
28
|
+
"Topic :: Utilities",
|
|
29
|
+
"Development Status :: 4 - Beta",
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"Operating System :: OS Independent",
|
|
32
|
+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
33
|
+
]
|
|
34
|
+
version = "20260531"
|
|
35
|
+
|
|
36
|
+
[project.license]
|
|
37
|
+
text = "GNU General Public License v3 or later (GPLv3+)"
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
"Monorepo Hg/Mercurial Mirror" = "https://hg.sr.ht/~cameron-simpson/css"
|
|
41
|
+
"Monorepo Git Mirror" = "https://github.com/cameron-simpson/css"
|
|
42
|
+
"MonoRepo Commits" = "https://bitbucket.org/cameron_simpson/css/commits/branch/main"
|
|
43
|
+
Source = "https://github.com/cameron-simpson/css/blob/main/lib/python/cs/pop3.py"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
pop3 = "cs.pop3:main"
|
|
47
|
+
|
|
48
|
+
[project.readme]
|
|
49
|
+
text = """
|
|
50
|
+
POP3 stuff, particularly a streaming downloader and a simple command line which runs it.
|
|
51
|
+
|
|
52
|
+
*Latest release 20260531*:
|
|
53
|
+
POP3Command.cmd_dl: add -1 (once) option for testing, notice SIGINT (though the RETRs get queued so fast it hardly helps), new --limit=n option to limit the number of messages fetched.
|
|
54
|
+
|
|
55
|
+
I spend some time on a geostationary satellite connection,
|
|
56
|
+
where round trip ping times are over 600ms when things are good.
|
|
57
|
+
|
|
58
|
+
My mail setup involves fetching messages from my inbox
|
|
59
|
+
for local storage in my laptop, usually using POP3.
|
|
60
|
+
The common standalone tools for this are `fetchmail` and `getmail`.
|
|
61
|
+
However, both are very subject to the link latency,
|
|
62
|
+
in that they request a message, collect it, issue a delete, then repeat.
|
|
63
|
+
On a satellite link that incurs a cost of over a second per message,
|
|
64
|
+
making catch up after a period offline a many minutes long exercise in tedium.
|
|
65
|
+
|
|
66
|
+
This module does something I've been meaning to do for literally years:
|
|
67
|
+
a bulk fetch. It issues `RETR`ieves for every message up front as fast as possible.
|
|
68
|
+
A separate thread collects the messages as they are delivered
|
|
69
|
+
and issues `DELE`tes for the saved messages as soon as each is saved.
|
|
70
|
+
|
|
71
|
+
This results in a fetch process which is orders of magnitude faster.
|
|
72
|
+
Even on a low latency link the throughput is much faster;
|
|
73
|
+
on the satellite it is gobsmackingly faster.
|
|
74
|
+
|
|
75
|
+
Short summary:
|
|
76
|
+
* `ConnectionSpec`: A specification for a POP3 connection.
|
|
77
|
+
* `main`: The `pop3` command line mode.
|
|
78
|
+
* `NetrcEntry`: A `namedtuple` representation of a `netrc` entry.
|
|
79
|
+
* `POP3`: Simple POP3 class with support for streaming use.
|
|
80
|
+
* `POP3Command`: Command line implementation for POP3 operations.
|
|
81
|
+
|
|
82
|
+
Module contents:
|
|
83
|
+
- <a name=\"ConnectionSpec\"></a>`class ConnectionSpec(ConnectionSpec)`: A specification for a POP3 connection.
|
|
84
|
+
|
|
85
|
+
*`ConnectionSpec.connect(self)`*:
|
|
86
|
+
Connect according to this `ConnectionSpec`, return the `socket`.
|
|
87
|
+
|
|
88
|
+
*`ConnectionSpec.from_spec(spec)`*:
|
|
89
|
+
Construct an instance from a connection spec string
|
|
90
|
+
of the form [`tcp:`|`ssl:`][*user*`@`]*[tcp_host!]server_hostname*[`:`*port*].
|
|
91
|
+
|
|
92
|
+
The optional prefixes `tcp:` and `ssl:` indicate that the connection
|
|
93
|
+
should be cleartext or SSL/TLS respectively.
|
|
94
|
+
The default is SSL/TLS.
|
|
95
|
+
|
|
96
|
+
*`ConnectionSpec.netrc_entry`*:
|
|
97
|
+
The default `NetrcEntry` for this `ConnectionSpec`.
|
|
98
|
+
|
|
99
|
+
*`ConnectionSpec.password`*:
|
|
100
|
+
The password for this connection, obtained from the `.netrc` file
|
|
101
|
+
via the key *user*`@`*host*`:`*port*.
|
|
102
|
+
- <a name=\"main\"></a>`main(argv=None)`: The `pop3` command line mode.
|
|
103
|
+
- <a name=\"NetrcEntry\"></a>`class NetrcEntry(NetrcEntry)`: A `namedtuple` representation of a `netrc` entry.
|
|
104
|
+
|
|
105
|
+
*`NetrcEntry.by_account(account_name, netrc_hosts=None)`*:
|
|
106
|
+
Look up an entry by the `account` field value.
|
|
107
|
+
|
|
108
|
+
*`NetrcEntry.get(machine, netrc_hosts=None)`*:
|
|
109
|
+
Look up an entry by the `machine` field value.
|
|
110
|
+
- <a name=\"POP3\"></a>`class POP3(cs.resources.MultiOpenMixin)`: Simple POP3 class with support for streaming use.
|
|
111
|
+
|
|
112
|
+
*`POP3.client_auth(self, user, password)`*:
|
|
113
|
+
Perform a client authentication.
|
|
114
|
+
|
|
115
|
+
*`POP3.client_begin(self)`*:
|
|
116
|
+
Read the opening server response.
|
|
117
|
+
|
|
118
|
+
*`POP3.client_bg(self, rq_line, is_multiline=False, notify=None)`*:
|
|
119
|
+
Dispatch a request `rq_line` in the background.
|
|
120
|
+
Return a `Result` to collect the request result.
|
|
121
|
+
|
|
122
|
+
Parameters:
|
|
123
|
+
* `rq_line`: POP3 request text, without any terminating CRLF
|
|
124
|
+
* `is_multiline`: true if a multiline response is expected,
|
|
125
|
+
default `False`
|
|
126
|
+
* `notify`: a optional handler for `Result.notify`,
|
|
127
|
+
applied if not `None`
|
|
128
|
+
|
|
129
|
+
*Note*: DOES NOT flush the send stream.
|
|
130
|
+
Call `self.flush()` when a batch of requests has been submitted,
|
|
131
|
+
before trying to collect the `Result`s.
|
|
132
|
+
|
|
133
|
+
The `Result` will receive `[etc,lines]` on success
|
|
134
|
+
where:
|
|
135
|
+
* `etc` is the trailing portion of an ok response line
|
|
136
|
+
* `lines` is a list of unstuffed text lines from the response
|
|
137
|
+
if `is_multiline` is true, `None` otherwise
|
|
138
|
+
The `Result` gets a list instead of a tuple
|
|
139
|
+
so that a handler may clear it in order to release memory.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
|
|
143
|
+
R = self.client_bg(f'RETR {msg_n}', is_multiline=True, notify=notify)
|
|
144
|
+
|
|
145
|
+
*`POP3.client_dele_bg(self, msg_n)`*:
|
|
146
|
+
Queue a delete request for message `msg_n`,
|
|
147
|
+
return ` Result` for collection.
|
|
148
|
+
|
|
149
|
+
*`POP3.client_quit_bg(self)`*:
|
|
150
|
+
Queue a QUIT request.
|
|
151
|
+
return ` Result` for collection.
|
|
152
|
+
|
|
153
|
+
*`POP3.client_retr_bg(self, msg_n, notify=None)`*:
|
|
154
|
+
Queue a retrieve request for message `msg_n`,
|
|
155
|
+
return ` Result` for collection.
|
|
156
|
+
|
|
157
|
+
If `notify` is not `None`, apply it to the `Result`.
|
|
158
|
+
|
|
159
|
+
*`POP3.client_uidl(self)`*:
|
|
160
|
+
Return a mapping of message number to message UID string.
|
|
161
|
+
|
|
162
|
+
*`POP3.dl_bg(self, msg_n, maildir, deleRs)`*:
|
|
163
|
+
Download message `msg_n` to Maildir `maildir`.
|
|
164
|
+
Return the `Result` for the `RETR` request.
|
|
165
|
+
|
|
166
|
+
After a successful save,
|
|
167
|
+
queue a `DELE` for the message
|
|
168
|
+
and add its `Result` to `deleRs`.
|
|
169
|
+
|
|
170
|
+
*`POP3.flush(self)`*:
|
|
171
|
+
Flush the send stream.
|
|
172
|
+
|
|
173
|
+
*`POP3.get_multiline(self)`*:
|
|
174
|
+
Generator yielding unstuffed lines from a multiline response.
|
|
175
|
+
|
|
176
|
+
*`POP3.get_ok(self)`*:
|
|
177
|
+
Read server response, require it to be `'OK+'`.
|
|
178
|
+
Returns the `etc` part.
|
|
179
|
+
|
|
180
|
+
*`POP3.get_response(self)`*:
|
|
181
|
+
Read a server response.
|
|
182
|
+
Return `(ok,status,etc)`
|
|
183
|
+
where `ok` is true if `status` is `'+OK'`, false otherwise;
|
|
184
|
+
`status` is the status word
|
|
185
|
+
and `etc` is the following text.
|
|
186
|
+
Return `(None,None,None)` on EOF from the receive stream.
|
|
187
|
+
|
|
188
|
+
*`POP3.readline(self)`*:
|
|
189
|
+
Read a CRLF terminated line from `self.recvf`.
|
|
190
|
+
Return the text preceeding the CRLF.
|
|
191
|
+
Return `None` at EOF.
|
|
192
|
+
|
|
193
|
+
*`POP3.readlines(self)`*:
|
|
194
|
+
Generator yielding lines from `self.recf`.
|
|
195
|
+
|
|
196
|
+
*`POP3.sendline(self, line, do_flush=False)`*:
|
|
197
|
+
Send a line (excluding its terminating CRLF).
|
|
198
|
+
If `do_flush` is true (default `False`)
|
|
199
|
+
also flush the sending stream.
|
|
200
|
+
|
|
201
|
+
*`POP3.startup_shutdown(self)`*:
|
|
202
|
+
Connect to the server and log in.
|
|
203
|
+
- <a name=\"POP3Command\"></a>`class POP3Command(cs.cmdutils.BaseCommand)`: Command line implementation for POP3 operations.
|
|
204
|
+
|
|
205
|
+
Credentials are obtained via the `.netrc` file presently.
|
|
206
|
+
|
|
207
|
+
Connection specifications consist of an optional leading mode prefix
|
|
208
|
+
followed by a netrc(5) account name
|
|
209
|
+
or an explicit connection specification
|
|
210
|
+
from which to derive:
|
|
211
|
+
* `user`: the user name to log in as
|
|
212
|
+
* `tcp_host`: the hostname to which to establish a TCP connection
|
|
213
|
+
* `port`: the TCP port to connect on, default 995 for TLS/SSL or 110 for cleartext
|
|
214
|
+
* `sni_host`: the TLS/SSL SNI server name, which may be different from the `tcp_host`
|
|
215
|
+
|
|
216
|
+
The optional mode prefix is one of:
|
|
217
|
+
* `ssl:`: use TLS/SSL - this is the default
|
|
218
|
+
* `tcp:`: use cleartext - this is useful for ssh port forwards
|
|
219
|
+
to some not-publicly-exposed clear text POP service;
|
|
220
|
+
in particular streaming performs better this way,
|
|
221
|
+
I think because the Python SSL layer does not buffer writes
|
|
222
|
+
|
|
223
|
+
Example connection specifications:
|
|
224
|
+
* `username@mail.example.com`:
|
|
225
|
+
use TLS/SSL to connect to the POP3S service at `mail.example.com`,
|
|
226
|
+
logging in as `username`
|
|
227
|
+
* `mail.example.com`:
|
|
228
|
+
use TLS/SSL to connect to the POP3S service at `mail.example.com`,
|
|
229
|
+
logging in with the same login as the local effective user
|
|
230
|
+
* `tcp:username@localhost:1110`:
|
|
231
|
+
use cleartext to connect to `localhost:1110`,
|
|
232
|
+
typically an ssh port forward to a remote private cleartext POP service,
|
|
233
|
+
logging in as `username`
|
|
234
|
+
* `username@localhost!mail.example.com:1995`:
|
|
235
|
+
use TLS/SSL to connect to `localhost:1995`,
|
|
236
|
+
usually an ssh port forward to a remote private TLS/SSL POP service,
|
|
237
|
+
logging in as `username` and passing `mail.exampl.com`
|
|
238
|
+
as the TLS/SSL server name indication
|
|
239
|
+
(which allows certificate verification to proceed correctly)
|
|
240
|
+
|
|
241
|
+
Note that the specification may also be a `netrc` account name.
|
|
242
|
+
If the specification matches such an account name
|
|
243
|
+
then values are derived from the `netrc` entry.
|
|
244
|
+
The entry's `machine` name becomes the TCP connection specification,
|
|
245
|
+
the entry's `login` provides a default for the username,
|
|
246
|
+
the entry's `account` host part provides the `sni_host`.
|
|
247
|
+
|
|
248
|
+
Example `netrc` entry:
|
|
249
|
+
|
|
250
|
+
machine username@localhost:1110
|
|
251
|
+
account username@mail.example.com
|
|
252
|
+
password ************
|
|
253
|
+
|
|
254
|
+
Such an entry allows you to use the specification `tcp:username@mail.example.com`
|
|
255
|
+
and obtain the remaining detail via the `netrc` entry.Credentials are obtained via the `.netrc` file presently.
|
|
256
|
+
|
|
257
|
+
Connection specifications consist of an optional leading mode prefix
|
|
258
|
+
followed by a netrc(5) account name
|
|
259
|
+
or an explicit connection specification
|
|
260
|
+
from which to derive:
|
|
261
|
+
* `user`: the user name to log in as
|
|
262
|
+
* `tcp_host`: the hostname to which to establish a TCP connection
|
|
263
|
+
* `port`: the TCP port to connect on, default 995 for TLS/SSL or 110 for cleartext
|
|
264
|
+
* `sni_host`: the TLS/SSL SNI server name, which may be different from the `tcp_host`
|
|
265
|
+
|
|
266
|
+
The optional mode prefix is one of:
|
|
267
|
+
* `ssl:`: use TLS/SSL - this is the default
|
|
268
|
+
* `tcp:`: use cleartext - this is useful for ssh port forwards
|
|
269
|
+
to some not-publicly-exposed clear text POP service;
|
|
270
|
+
in particular streaming performs better this way,
|
|
271
|
+
I think because the Python SSL layer does not buffer writes
|
|
272
|
+
|
|
273
|
+
Example connection specifications:
|
|
274
|
+
* `username@mail.example.com`:
|
|
275
|
+
use TLS/SSL to connect to the POP3S service at `mail.example.com`,
|
|
276
|
+
logging in as `username`
|
|
277
|
+
* `mail.example.com`:
|
|
278
|
+
use TLS/SSL to connect to the POP3S service at `mail.example.com`,
|
|
279
|
+
logging in with the same login as the local effective user
|
|
280
|
+
* `tcp:username@localhost:1110`:
|
|
281
|
+
use cleartext to connect to `localhost:1110`,
|
|
282
|
+
typically an ssh port forward to a remote private cleartext POP service,
|
|
283
|
+
logging in as `username`
|
|
284
|
+
* `username@localhost!mail.example.com:1995`:
|
|
285
|
+
use TLS/SSL to connect to `localhost:1995`,
|
|
286
|
+
usually an ssh port forward to a remote private TLS/SSL POP service,
|
|
287
|
+
logging in as `username` and passing `mail.exampl.com`
|
|
288
|
+
as the TLS/SSL server name indication
|
|
289
|
+
(which allows certificate verification to proceed correctly)
|
|
290
|
+
|
|
291
|
+
Note that the specification may also be a `netrc` account name.
|
|
292
|
+
If the specification matches such an account name
|
|
293
|
+
then values are derived from the `netrc` entry.
|
|
294
|
+
The entry's `machine` name becomes the TCP connection specification,
|
|
295
|
+
the entry's `login` provides a default for the username,
|
|
296
|
+
the entry's `account` host part provides the `sni_host`.
|
|
297
|
+
|
|
298
|
+
Example `netrc` entry:
|
|
299
|
+
|
|
300
|
+
machine username@localhost:1110
|
|
301
|
+
account username@mail.example.com
|
|
302
|
+
password ************
|
|
303
|
+
|
|
304
|
+
Such an entry allows you to use the specification `tcp:username@mail.example.com`
|
|
305
|
+
and obtain the remaining detail via the `netrc` entry.
|
|
306
|
+
|
|
307
|
+
Usage summary:
|
|
308
|
+
|
|
309
|
+
Usage: pop3 [common-options...] subcommand [options...]
|
|
310
|
+
Command line implementation for POP3 operations.
|
|
311
|
+
Subcommands:
|
|
312
|
+
dl [common-options...] [{ssl,tcp}:]{netrc_account|[user@]host[!sni_name][:port]} maildir
|
|
313
|
+
Collect messages from a POP3 server and deliver to a Maildir.
|
|
314
|
+
Options:
|
|
315
|
+
-1 Download only 1 message.
|
|
316
|
+
--limit limit Limit the number of messages fetched.
|
|
317
|
+
help [common-options...] [-l] [-s] [subcommand-names...]
|
|
318
|
+
Print help for subcommands.
|
|
319
|
+
This outputs the full help for the named subcommands,
|
|
320
|
+
or the short help for all subcommands if no names are specified.
|
|
321
|
+
Options:
|
|
322
|
+
-l Long listing.
|
|
323
|
+
-r Recurse into subcommands.
|
|
324
|
+
-s Short listing.
|
|
325
|
+
info [common-options...] [field-names...]
|
|
326
|
+
Recite general information.
|
|
327
|
+
Explicit field names may be provided to override the default listing.
|
|
328
|
+
repl [common-options...]
|
|
329
|
+
Run a REPL (Read Evaluate Print Loop), an interactive Python prompt.
|
|
330
|
+
Options:
|
|
331
|
+
--banner banner Banner.
|
|
332
|
+
shell [common-options...]
|
|
333
|
+
Run a command prompt via cmd.Cmd using this command's subcommands.
|
|
334
|
+
|
|
335
|
+
*`POP3Command.cmd_dl(self, argv)`*:
|
|
336
|
+
Usage: {cmd} [{{ssl,tcp}}:]{{netrc_account|[user@]host[!sni_name][:port]}} maildir
|
|
337
|
+
Collect messages from a POP3 server and deliver to a Maildir.
|
|
338
|
+
Options:
|
|
339
|
+
-1 Download only 1 message.
|
|
340
|
+
--limit limit Limit the number of messages fetched.
|
|
341
|
+
|
|
342
|
+
# Release Log
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
*Release 20260531*:
|
|
347
|
+
POP3Command.cmd_dl: add -1 (once) option for testing, notice SIGINT (though the RETRs get queued so fast it hardly helps), new --limit=n option to limit the number of messages fetched.
|
|
348
|
+
|
|
349
|
+
*Release 20240316*:
|
|
350
|
+
Fixed release upload artifacts.
|
|
351
|
+
|
|
352
|
+
*Release 20240201.1*:
|
|
353
|
+
Another test release, nothing new.
|
|
354
|
+
|
|
355
|
+
*Release 20240201*:
|
|
356
|
+
Test release with better DISTINFO.
|
|
357
|
+
|
|
358
|
+
*Release 20221221*:
|
|
359
|
+
Fix stray %s in format string, modernise MultiOpenMixin startup/shutdown, catch ConnectionRefusedError and report succintly.
|
|
360
|
+
|
|
361
|
+
*Release 20220918*:
|
|
362
|
+
* Emit an error instead of stack trace for messages which cannot be saved (and do not delete).
|
|
363
|
+
* POP3Command.cmd_dl: new -n (no action) option.
|
|
364
|
+
|
|
365
|
+
*Release 20220606*:
|
|
366
|
+
Minor updates.
|
|
367
|
+
|
|
368
|
+
*Release 20220312*:
|
|
369
|
+
Make POP3Command.cmd_dl an instance method (static methods broke with the latest cs.cmdutils release).
|
|
370
|
+
|
|
371
|
+
*Release 20211208*:
|
|
372
|
+
* POP3.startup: do not start the worker queue until authenticated.
|
|
373
|
+
* POP3.get_response: return (None,None,None) at EOF.
|
|
374
|
+
* POP3.shutdown: catch exceptions from client QUIT.
|
|
375
|
+
|
|
376
|
+
*Release 20210407.2*:
|
|
377
|
+
Provide \"pop3\" console_script.
|
|
378
|
+
|
|
379
|
+
*Release 20210407.1*:
|
|
380
|
+
Bump for cs.cmdutils minor bugfix, also fix a few docstring typos.
|
|
381
|
+
|
|
382
|
+
*Release 20210407*:
|
|
383
|
+
Initial PyPI release."""
|
|
384
|
+
content-type = "text/markdown"
|
|
385
|
+
|
|
386
|
+
[build-system]
|
|
387
|
+
build-backend = "flit_core.buildapi"
|
|
388
|
+
requires = [
|
|
389
|
+
"flit_core >=3.2,<4",
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
[tool.flit.module]
|
|
393
|
+
name = "cs.pop3"
|
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
''' POP3 stuff, particularly a streaming downloader and a simple command line which runs it.
|
|
4
|
+
|
|
5
|
+
I spend some time on a geostationary satellite connection,
|
|
6
|
+
where round trip ping times are over 600ms when things are good.
|
|
7
|
+
|
|
8
|
+
My mail setup involves fetching messages from my inbox
|
|
9
|
+
for local storage in my laptop, usually using POP3.
|
|
10
|
+
The common standalone tools for this are `fetchmail` and `getmail`.
|
|
11
|
+
However, both are very subject to the link latency,
|
|
12
|
+
in that they request a message, collect it, issue a delete, then repeat.
|
|
13
|
+
On a satellite link that incurs a cost of over a second per message,
|
|
14
|
+
making catch up after a period offline a many minutes long exercise in tedium.
|
|
15
|
+
|
|
16
|
+
This module does something I've been meaning to do for literally years:
|
|
17
|
+
a bulk fetch. It issues `RETR`ieves for every message up front as fast as possible.
|
|
18
|
+
A separate thread collects the messages as they are delivered
|
|
19
|
+
and issues `DELE`tes for the saved messages as soon as each is saved.
|
|
20
|
+
|
|
21
|
+
This results in a fetch process which is orders of magnitude faster.
|
|
22
|
+
Even on a low latency link the throughput is much faster;
|
|
23
|
+
on the satellite it is gobsmackingly faster.
|
|
24
|
+
'''
|
|
25
|
+
|
|
26
|
+
from collections import namedtuple
|
|
27
|
+
from contextlib import contextmanager
|
|
28
|
+
from email.parser import BytesParser
|
|
29
|
+
from getopt import GetoptError
|
|
30
|
+
from mailbox import Maildir
|
|
31
|
+
from netrc import netrc
|
|
32
|
+
from os import geteuid
|
|
33
|
+
from os.path import isdir as isdirpath
|
|
34
|
+
from pwd import getpwuid
|
|
35
|
+
from socket import create_connection
|
|
36
|
+
import ssl
|
|
37
|
+
import sys
|
|
38
|
+
from threading import RLock
|
|
39
|
+
|
|
40
|
+
from cs.cmdutils import BaseCommand, popopts
|
|
41
|
+
from cs.fs import shortpath
|
|
42
|
+
from cs.lex import cutprefix, cutsuffix
|
|
43
|
+
from cs.logutils import debug, warning, error, exception
|
|
44
|
+
from cs.pfx import Pfx, pfx_call
|
|
45
|
+
from cs.queues import IterableQueue
|
|
46
|
+
from cs.resources import MultiOpenMixin
|
|
47
|
+
from cs.result import Result, ResultSet
|
|
48
|
+
from cs.threads import bg as bg_thread
|
|
49
|
+
from cs.upd import print
|
|
50
|
+
|
|
51
|
+
__version__ = '20260531'
|
|
52
|
+
|
|
53
|
+
DISTINFO = {
|
|
54
|
+
'keywords': ["python3"],
|
|
55
|
+
'classifiers': [
|
|
56
|
+
"Programming Language :: Python",
|
|
57
|
+
"Programming Language :: Python :: 3",
|
|
58
|
+
"Environment :: Console",
|
|
59
|
+
"Topic :: Communications :: Email :: Post-Office :: POP3",
|
|
60
|
+
"Topic :: Internet",
|
|
61
|
+
"Topic :: Utilities",
|
|
62
|
+
],
|
|
63
|
+
'install_requires': [
|
|
64
|
+
'cs.cmdutils>=20210407.1',
|
|
65
|
+
'cs.fs',
|
|
66
|
+
'cs.lex',
|
|
67
|
+
'cs.logutils',
|
|
68
|
+
'cs.pfx',
|
|
69
|
+
'cs.queues',
|
|
70
|
+
'cs.resources',
|
|
71
|
+
'cs.result>=20210407',
|
|
72
|
+
'cs.threads',
|
|
73
|
+
'cs.upd',
|
|
74
|
+
],
|
|
75
|
+
'entry_points': {
|
|
76
|
+
'console_scripts': {
|
|
77
|
+
'pop3': 'cs.pop3:main'
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def main(argv=None):
|
|
83
|
+
''' The `pop3` command line mode.
|
|
84
|
+
'''
|
|
85
|
+
return POP3Command(argv).run()
|
|
86
|
+
|
|
87
|
+
class POP3(MultiOpenMixin):
|
|
88
|
+
''' Simple POP3 class with support for streaming use.
|
|
89
|
+
'''
|
|
90
|
+
|
|
91
|
+
def __init__(self, conn_spec):
|
|
92
|
+
if isinstance(conn_spec, str):
|
|
93
|
+
conn_spec = ConnectionSpec.from_spec(conn_spec)
|
|
94
|
+
self.conn_spec = conn_spec
|
|
95
|
+
self._result_queue = None
|
|
96
|
+
self._client_worker = None
|
|
97
|
+
self._sock = None
|
|
98
|
+
self.recvf = None
|
|
99
|
+
self.sendf = None
|
|
100
|
+
self._lock = RLock()
|
|
101
|
+
|
|
102
|
+
@contextmanager
|
|
103
|
+
def startup_shutdown(self):
|
|
104
|
+
''' Connect to the server and log in.
|
|
105
|
+
'''
|
|
106
|
+
self._sock = self.conn_spec.connect()
|
|
107
|
+
self.recvf = self._sock.makefile('r', encoding='iso8859-1')
|
|
108
|
+
self.sendf = self._sock.makefile('w', encoding='ascii')
|
|
109
|
+
self.client_begin()
|
|
110
|
+
self.client_auth(self.conn_spec.user, self.conn_spec.password)
|
|
111
|
+
self._result_queue = IterableQueue()
|
|
112
|
+
self._client_worker = bg_thread(
|
|
113
|
+
self._client_response_worker, args=(self._result_queue,)
|
|
114
|
+
)
|
|
115
|
+
try:
|
|
116
|
+
yield
|
|
117
|
+
finally:
|
|
118
|
+
logmsg = debug
|
|
119
|
+
logmsg("send client QUIT")
|
|
120
|
+
try:
|
|
121
|
+
quitR = self.client_quit_bg()
|
|
122
|
+
logmsg("flush QUIT")
|
|
123
|
+
self.flush()
|
|
124
|
+
logmsg("join QUIT")
|
|
125
|
+
quitR.join()
|
|
126
|
+
except Exception as e: # pylint: disable=broad-except
|
|
127
|
+
exception("client quit: %s", e)
|
|
128
|
+
logmsg = warning
|
|
129
|
+
if self._result_queue:
|
|
130
|
+
logmsg("close result queue")
|
|
131
|
+
self._result_queue.close()
|
|
132
|
+
self._result_queue = None
|
|
133
|
+
if self._client_worker:
|
|
134
|
+
logmsg("join client worker")
|
|
135
|
+
self._client_worker.join()
|
|
136
|
+
self._client_worker = None
|
|
137
|
+
logmsg("close sendf")
|
|
138
|
+
self.sendf.close()
|
|
139
|
+
self.sendf = None
|
|
140
|
+
logmsg("check for uncollected server responses")
|
|
141
|
+
bs = self.recvf.read()
|
|
142
|
+
if bs:
|
|
143
|
+
warning("received %d bytes from the server at shutdown", len(bs))
|
|
144
|
+
logmsg("close recvf")
|
|
145
|
+
self.recvf.close()
|
|
146
|
+
self.recvf = None
|
|
147
|
+
logmsg("close socket")
|
|
148
|
+
self._sock.close()
|
|
149
|
+
self._sock = None
|
|
150
|
+
logmsg("shutdown complete")
|
|
151
|
+
|
|
152
|
+
def readline(self):
|
|
153
|
+
''' Read a CRLF terminated line from `self.recvf`.
|
|
154
|
+
Return the text preceeding the CRLF.
|
|
155
|
+
Return `None` at EOF.
|
|
156
|
+
'''
|
|
157
|
+
line0 = self.recvf.readline()
|
|
158
|
+
if not line0:
|
|
159
|
+
return None
|
|
160
|
+
line = cutsuffix(line0, '\n')
|
|
161
|
+
assert line is not line0, "missing LF: %r" % (line0,)
|
|
162
|
+
line = cutsuffix(line, '\r')
|
|
163
|
+
return line
|
|
164
|
+
|
|
165
|
+
def readlines(self):
|
|
166
|
+
''' Generator yielding lines from `self.recf`.
|
|
167
|
+
'''
|
|
168
|
+
while True:
|
|
169
|
+
line = self.readline()
|
|
170
|
+
if line is None:
|
|
171
|
+
break
|
|
172
|
+
yield line
|
|
173
|
+
|
|
174
|
+
def get_response(self):
|
|
175
|
+
''' Read a server response.
|
|
176
|
+
Return `(ok,status,etc)`
|
|
177
|
+
where `ok` is true if `status` is `'+OK'`, false otherwise;
|
|
178
|
+
`status` is the status word
|
|
179
|
+
and `etc` is the following text.
|
|
180
|
+
Return `(None,None,None)` on EOF from the receive stream.
|
|
181
|
+
'''
|
|
182
|
+
line = self.readline()
|
|
183
|
+
if line is None:
|
|
184
|
+
return None, None, None
|
|
185
|
+
try:
|
|
186
|
+
status, etc = line.split(None, 1)
|
|
187
|
+
except ValueError:
|
|
188
|
+
status = line
|
|
189
|
+
etc = ''
|
|
190
|
+
return status == '+OK', status, etc
|
|
191
|
+
|
|
192
|
+
def get_ok(self):
|
|
193
|
+
''' Read server response, require it to be `'OK+'`.
|
|
194
|
+
Returns the `etc` part.
|
|
195
|
+
'''
|
|
196
|
+
ok, status, etc = self.get_response()
|
|
197
|
+
if not ok:
|
|
198
|
+
raise ValueError("no ok from server: %r %r" % (status, etc))
|
|
199
|
+
return etc
|
|
200
|
+
|
|
201
|
+
def get_multiline(self):
|
|
202
|
+
''' Generator yielding unstuffed lines from a multiline response.
|
|
203
|
+
'''
|
|
204
|
+
for line in self.readlines():
|
|
205
|
+
if line == '.':
|
|
206
|
+
break
|
|
207
|
+
if line.startswith('.'):
|
|
208
|
+
line = line[1:]
|
|
209
|
+
yield line
|
|
210
|
+
|
|
211
|
+
def flush(self):
|
|
212
|
+
''' Flush the send stream.
|
|
213
|
+
'''
|
|
214
|
+
self.sendf.flush()
|
|
215
|
+
|
|
216
|
+
def sendline(self, line, do_flush=False):
|
|
217
|
+
''' Send a line (excluding its terminating CRLF).
|
|
218
|
+
If `do_flush` is true (default `False`)
|
|
219
|
+
also flush the sending stream.
|
|
220
|
+
'''
|
|
221
|
+
assert '\r' not in line and '\n' not in line
|
|
222
|
+
self.sendf.write(line)
|
|
223
|
+
self.sendf.write('\r\n')
|
|
224
|
+
if do_flush:
|
|
225
|
+
self.flush()
|
|
226
|
+
|
|
227
|
+
def _client_response_worker(self, result_queue):
|
|
228
|
+
''' Worker to process queued request responses.
|
|
229
|
+
Each completed response assigns `(etc,lines)` to the `Result`
|
|
230
|
+
where `etc` is the addition text from the server ok response
|
|
231
|
+
and `lines` is a list of the multiline part of the response
|
|
232
|
+
or `None` if the response is not multiline.
|
|
233
|
+
'''
|
|
234
|
+
for R, is_multiline in result_queue:
|
|
235
|
+
try:
|
|
236
|
+
etc = self.get_ok()
|
|
237
|
+
if is_multiline:
|
|
238
|
+
lines = list(self.get_multiline())
|
|
239
|
+
else:
|
|
240
|
+
lines = None
|
|
241
|
+
except Exception as e: # pylint: disable=broad-except
|
|
242
|
+
R.exc_info = sys.exc_info
|
|
243
|
+
warning("%s: %s", R, e)
|
|
244
|
+
else:
|
|
245
|
+
# save a list so that we can erase it in a handler to release memory
|
|
246
|
+
R.result = [etc, lines]
|
|
247
|
+
|
|
248
|
+
def client_begin(self):
|
|
249
|
+
''' Read the opening server response.
|
|
250
|
+
'''
|
|
251
|
+
etc = self.get_ok()
|
|
252
|
+
print(etc)
|
|
253
|
+
|
|
254
|
+
def client_auth(self, user, password):
|
|
255
|
+
''' Perform a client authentication.
|
|
256
|
+
'''
|
|
257
|
+
self.sendline(f'USER {user}', do_flush=True)
|
|
258
|
+
print('USER', user, self.get_ok())
|
|
259
|
+
self.sendline(f'PASS {password}', do_flush=True)
|
|
260
|
+
print('PASS', '****', self.get_ok())
|
|
261
|
+
|
|
262
|
+
def client_uidl(self):
|
|
263
|
+
''' Return a mapping of message number to message UID string.
|
|
264
|
+
'''
|
|
265
|
+
self.sendline('UIDL', do_flush=True)
|
|
266
|
+
self.get_ok()
|
|
267
|
+
for line in self.get_multiline():
|
|
268
|
+
n, msg_uid = line.split(None, 1)
|
|
269
|
+
n = int(n)
|
|
270
|
+
yield n, msg_uid
|
|
271
|
+
|
|
272
|
+
def client_bg(self, rq_line, is_multiline=False, notify=None):
|
|
273
|
+
''' Dispatch a request `rq_line` in the background.
|
|
274
|
+
Return a `Result` to collect the request result.
|
|
275
|
+
|
|
276
|
+
Parameters:
|
|
277
|
+
* `rq_line`: POP3 request text, without any terminating CRLF
|
|
278
|
+
* `is_multiline`: true if a multiline response is expected,
|
|
279
|
+
default `False`
|
|
280
|
+
* `notify`: a optional handler for `Result.notify`,
|
|
281
|
+
applied if not `None`
|
|
282
|
+
|
|
283
|
+
*Note*: DOES NOT flush the send stream.
|
|
284
|
+
Call `self.flush()` when a batch of requests has been submitted,
|
|
285
|
+
before trying to collect the `Result`s.
|
|
286
|
+
|
|
287
|
+
The `Result` will receive `[etc,lines]` on success
|
|
288
|
+
where:
|
|
289
|
+
* `etc` is the trailing portion of an ok response line
|
|
290
|
+
* `lines` is a list of unstuffed text lines from the response
|
|
291
|
+
if `is_multiline` is true, `None` otherwise
|
|
292
|
+
The `Result` gets a list instead of a tuple
|
|
293
|
+
so that a handler may clear it in order to release memory.
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
|
|
297
|
+
R = self.client_bg(f'RETR {msg_n}', is_multiline=True, notify=notify)
|
|
298
|
+
'''
|
|
299
|
+
with self._lock:
|
|
300
|
+
self.sendline(rq_line)
|
|
301
|
+
R = Result(rq_line)
|
|
302
|
+
self._result_queue.put((R, is_multiline))
|
|
303
|
+
R.extra.update(rq_line=rq_line)
|
|
304
|
+
if notify is not None:
|
|
305
|
+
R.notify(notify)
|
|
306
|
+
return R
|
|
307
|
+
|
|
308
|
+
def client_dele_bg(self, msg_n):
|
|
309
|
+
''' Queue a delete request for message `msg_n`,
|
|
310
|
+
return ` Result` for collection.
|
|
311
|
+
'''
|
|
312
|
+
R = self.client_bg(f'DELE {msg_n}')
|
|
313
|
+
R.extra.update(msg_n=msg_n)
|
|
314
|
+
return R
|
|
315
|
+
|
|
316
|
+
def client_quit_bg(self):
|
|
317
|
+
''' Queue a QUIT request.
|
|
318
|
+
return ` Result` for collection.
|
|
319
|
+
'''
|
|
320
|
+
R = self.client_bg('QUIT')
|
|
321
|
+
return R
|
|
322
|
+
|
|
323
|
+
def client_retr_bg(self, msg_n, notify=None):
|
|
324
|
+
''' Queue a retrieve request for message `msg_n`,
|
|
325
|
+
return ` Result` for collection.
|
|
326
|
+
|
|
327
|
+
If `notify` is not `None`, apply it to the `Result`.
|
|
328
|
+
'''
|
|
329
|
+
R = self.client_bg(f'RETR {msg_n}', is_multiline=True, notify=notify)
|
|
330
|
+
R.extra.update(msg_n=msg_n)
|
|
331
|
+
return R
|
|
332
|
+
|
|
333
|
+
def dl_bg(self, msg_n, maildir, deleRs):
|
|
334
|
+
''' Download message `msg_n` to Maildir `maildir`.
|
|
335
|
+
Return the `Result` for the `RETR` request.
|
|
336
|
+
|
|
337
|
+
After a successful save,
|
|
338
|
+
queue a `DELE` for the message
|
|
339
|
+
and add its `Result` to `deleRs`.
|
|
340
|
+
'''
|
|
341
|
+
|
|
342
|
+
def dl_bg_save_result(R):
|
|
343
|
+
with Pfx("MSG %d", msg_n):
|
|
344
|
+
_, lines = R.result
|
|
345
|
+
R.result[1] = None # release lines
|
|
346
|
+
msg_bs = b''.join(
|
|
347
|
+
map(lambda line: line.encode('iso8859-1') + b'\r\n', lines)
|
|
348
|
+
)
|
|
349
|
+
msg = BytesParser().parsebytes(msg_bs)
|
|
350
|
+
hdr_from = str(msg.get('from', '<UNKNOWN>'))
|
|
351
|
+
with Pfx("from %s", hdr_from):
|
|
352
|
+
try:
|
|
353
|
+
# mailbox.Maildir.add is not thread safe, serialise it
|
|
354
|
+
with self._lock:
|
|
355
|
+
Mkey = maildir.add(msg)
|
|
356
|
+
except UnicodeEncodeError as e:
|
|
357
|
+
error(
|
|
358
|
+
"cannot save to %s, skipping DELE: %s",
|
|
359
|
+
shortpath(maildir._path), e
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
print(
|
|
363
|
+
f'msg {msg_n} from {hdr_from}: {len(msg_bs)} octets, saved as {Mkey}, deleting'
|
|
364
|
+
)
|
|
365
|
+
if deleRs is not None:
|
|
366
|
+
deleRs.add(self.client_dele_bg(msg_n))
|
|
367
|
+
|
|
368
|
+
R = self.client_retr_bg(msg_n, notify=dl_bg_save_result)
|
|
369
|
+
return R
|
|
370
|
+
|
|
371
|
+
class NetrcEntry(namedtuple('NetrcEntry', 'machine login account password')):
|
|
372
|
+
''' A `namedtuple` representation of a `netrc` entry.
|
|
373
|
+
'''
|
|
374
|
+
|
|
375
|
+
NO_ENTRY = None, None, None
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def get(cls, machine, netrc_hosts=None):
|
|
379
|
+
''' Look up an entry by the `machine` field value.
|
|
380
|
+
'''
|
|
381
|
+
if netrc_hosts is None:
|
|
382
|
+
netrc_hosts = netrc().hosts
|
|
383
|
+
entry = netrc_hosts.get(machine, cls.NO_ENTRY)
|
|
384
|
+
return cls(machine, *entry)
|
|
385
|
+
|
|
386
|
+
@classmethod
|
|
387
|
+
def by_account(cls, account_name, netrc_hosts=None):
|
|
388
|
+
''' Look up an entry by the `account` field value.
|
|
389
|
+
'''
|
|
390
|
+
if netrc_hosts is None:
|
|
391
|
+
netrc_hosts = netrc().hosts
|
|
392
|
+
for machine, entry_tuple in netrc_hosts.items():
|
|
393
|
+
if entry_tuple[1] == account_name:
|
|
394
|
+
return cls(machine, *entry_tuple)
|
|
395
|
+
return cls(None, *cls.NO_ENTRY)
|
|
396
|
+
|
|
397
|
+
class ConnectionSpec(namedtuple('ConnectionSpec',
|
|
398
|
+
'user host sni_host port ssl')):
|
|
399
|
+
''' A specification for a POP3 connection.
|
|
400
|
+
'''
|
|
401
|
+
|
|
402
|
+
# pylint: disable=too-many-branches
|
|
403
|
+
@classmethod
|
|
404
|
+
def from_spec(cls, spec):
|
|
405
|
+
''' Construct an instance from a connection spec string
|
|
406
|
+
of the form [`tcp:`|`ssl:`][*user*`@`]*[tcp_host!]server_hostname*[`:`*port*].
|
|
407
|
+
|
|
408
|
+
The optional prefixes `tcp:` and `ssl:` indicate that the connection
|
|
409
|
+
should be cleartext or SSL/TLS respectively.
|
|
410
|
+
The default is SSL/TLS.
|
|
411
|
+
'''
|
|
412
|
+
spec2 = cutprefix(spec, 'tcp:')
|
|
413
|
+
if spec2 is not spec:
|
|
414
|
+
spec = spec2
|
|
415
|
+
use_ssl = False
|
|
416
|
+
else:
|
|
417
|
+
spec = cutprefix(spec, 'ssl:')
|
|
418
|
+
use_ssl = True
|
|
419
|
+
# see if what's left after the mode matches a netrc account name
|
|
420
|
+
account_entry = NetrcEntry.by_account(spec)
|
|
421
|
+
if account_entry.machine is None:
|
|
422
|
+
account_entry = None
|
|
423
|
+
else:
|
|
424
|
+
# a match, use the machine name as the spec
|
|
425
|
+
spec = account_entry.machine
|
|
426
|
+
try:
|
|
427
|
+
user, hostpart = spec.split('@', 1)
|
|
428
|
+
except ValueError:
|
|
429
|
+
# no user specified, use a default
|
|
430
|
+
hostpart = spec
|
|
431
|
+
current_user = getpwuid(geteuid()).pw_name
|
|
432
|
+
if account_entry:
|
|
433
|
+
if account_entry.login:
|
|
434
|
+
user = account_entry.login
|
|
435
|
+
else:
|
|
436
|
+
# see if the account name has a user part
|
|
437
|
+
try:
|
|
438
|
+
user, _ = account_entry.account.split('@', 1)
|
|
439
|
+
except ValueError:
|
|
440
|
+
user = current_user
|
|
441
|
+
else:
|
|
442
|
+
user = current_user
|
|
443
|
+
try:
|
|
444
|
+
host, port = hostpart.split(':')
|
|
445
|
+
except ValueError:
|
|
446
|
+
host = hostpart
|
|
447
|
+
port = 995 if use_ssl else 110
|
|
448
|
+
else:
|
|
449
|
+
port = int(port)
|
|
450
|
+
try:
|
|
451
|
+
tcp_host, sni_host = host.split('!', 1)
|
|
452
|
+
except ValueError:
|
|
453
|
+
# get the SNI name from the account name
|
|
454
|
+
if account_entry:
|
|
455
|
+
tcp_host = host
|
|
456
|
+
try:
|
|
457
|
+
_, sni_host = account_entry.account.split('@', 1)
|
|
458
|
+
except ValueError:
|
|
459
|
+
sni_host = account_entry.account
|
|
460
|
+
else:
|
|
461
|
+
tcp_host, sni_host = host, host
|
|
462
|
+
conn_spec = cls(
|
|
463
|
+
user=user, host=tcp_host, sni_host=sni_host, port=port, ssl=use_ssl
|
|
464
|
+
)
|
|
465
|
+
##print("conn_spec =", conn_spec)
|
|
466
|
+
return conn_spec
|
|
467
|
+
|
|
468
|
+
@property
|
|
469
|
+
def netrc_entry(self):
|
|
470
|
+
''' The default `NetrcEntry` for this `ConnectionSpec`.
|
|
471
|
+
'''
|
|
472
|
+
machine = f'{self.user}@{self.host}:{self.port}'
|
|
473
|
+
return NetrcEntry.get(machine)
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def password(self):
|
|
477
|
+
''' The password for this connection, obtained from the `.netrc` file
|
|
478
|
+
via the key *user*`@`*host*`:`*port*.
|
|
479
|
+
'''
|
|
480
|
+
entry = self.netrc_entry
|
|
481
|
+
return entry.password
|
|
482
|
+
|
|
483
|
+
def connect(self):
|
|
484
|
+
''' Connect according to this `ConnectionSpec`, return the `socket`.
|
|
485
|
+
'''
|
|
486
|
+
sock = pfx_call(create_connection, (self.host, self.port))
|
|
487
|
+
if self.ssl:
|
|
488
|
+
context = ssl.create_default_context()
|
|
489
|
+
sock = context.wrap_socket(sock, server_hostname=self.sni_host)
|
|
490
|
+
print("SSL:", sock.version())
|
|
491
|
+
return sock
|
|
492
|
+
|
|
493
|
+
class POP3Command(BaseCommand):
|
|
494
|
+
''' Command line implementation for POP3 operations.
|
|
495
|
+
|
|
496
|
+
Credentials are obtained via the `.netrc` file presently.
|
|
497
|
+
|
|
498
|
+
Connection specifications consist of an optional leading mode prefix
|
|
499
|
+
followed by a netrc(5) account name
|
|
500
|
+
or an explicit connection specification
|
|
501
|
+
from which to derive:
|
|
502
|
+
* `user`: the user name to log in as
|
|
503
|
+
* `tcp_host`: the hostname to which to establish a TCP connection
|
|
504
|
+
* `port`: the TCP port to connect on, default 995 for TLS/SSL or 110 for cleartext
|
|
505
|
+
* `sni_host`: the TLS/SSL SNI server name, which may be different from the `tcp_host`
|
|
506
|
+
|
|
507
|
+
The optional mode prefix is one of:
|
|
508
|
+
* `ssl:`: use TLS/SSL - this is the default
|
|
509
|
+
* `tcp:`: use cleartext - this is useful for ssh port forwards
|
|
510
|
+
to some not-publicly-exposed clear text POP service;
|
|
511
|
+
in particular streaming performs better this way,
|
|
512
|
+
I think because the Python SSL layer does not buffer writes
|
|
513
|
+
|
|
514
|
+
Example connection specifications:
|
|
515
|
+
* `username@mail.example.com`:
|
|
516
|
+
use TLS/SSL to connect to the POP3S service at `mail.example.com`,
|
|
517
|
+
logging in as `username`
|
|
518
|
+
* `mail.example.com`:
|
|
519
|
+
use TLS/SSL to connect to the POP3S service at `mail.example.com`,
|
|
520
|
+
logging in with the same login as the local effective user
|
|
521
|
+
* `tcp:username@localhost:1110`:
|
|
522
|
+
use cleartext to connect to `localhost:1110`,
|
|
523
|
+
typically an ssh port forward to a remote private cleartext POP service,
|
|
524
|
+
logging in as `username`
|
|
525
|
+
* `username@localhost!mail.example.com:1995`:
|
|
526
|
+
use TLS/SSL to connect to `localhost:1995`,
|
|
527
|
+
usually an ssh port forward to a remote private TLS/SSL POP service,
|
|
528
|
+
logging in as `username` and passing `mail.exampl.com`
|
|
529
|
+
as the TLS/SSL server name indication
|
|
530
|
+
(which allows certificate verification to proceed correctly)
|
|
531
|
+
|
|
532
|
+
Note that the specification may also be a `netrc` account name.
|
|
533
|
+
If the specification matches such an account name
|
|
534
|
+
then values are derived from the `netrc` entry.
|
|
535
|
+
The entry's `machine` name becomes the TCP connection specification,
|
|
536
|
+
the entry's `login` provides a default for the username,
|
|
537
|
+
the entry's `account` host part provides the `sni_host`.
|
|
538
|
+
|
|
539
|
+
Example `netrc` entry:
|
|
540
|
+
|
|
541
|
+
machine username@localhost:1110
|
|
542
|
+
account username@mail.example.com
|
|
543
|
+
password ************
|
|
544
|
+
|
|
545
|
+
Such an entry allows you to use the specification `tcp:username@mail.example.com`
|
|
546
|
+
and obtain the remaining detail via the `netrc` entry.
|
|
547
|
+
'''
|
|
548
|
+
|
|
549
|
+
# pylint: disable=no-self-use,too-many-locals
|
|
550
|
+
@popopts(
|
|
551
|
+
_1=('once', "Download only 1 message."),
|
|
552
|
+
limit_=("Limit the number of messages fetched.", int),
|
|
553
|
+
)
|
|
554
|
+
def cmd_dl(self, argv):
|
|
555
|
+
''' Usage: {cmd} [{{ssl,tcp}}:]{{netrc_account|[user@]host[!sni_name][:port]}} maildir
|
|
556
|
+
Collect messages from a POP3 server and deliver to a Maildir.
|
|
557
|
+
'''
|
|
558
|
+
doit = self.options.doit
|
|
559
|
+
once = self.options.once
|
|
560
|
+
limit = self.options.limit
|
|
561
|
+
runstate = self.options.runstate
|
|
562
|
+
pop_target = argv.pop(0)
|
|
563
|
+
maildir_path = argv.pop(0)
|
|
564
|
+
if argv:
|
|
565
|
+
raise GetoptError("extra arguments after maildir: %r", argv)
|
|
566
|
+
with Pfx("maildir %r", maildir_path):
|
|
567
|
+
if not isdirpath(maildir_path):
|
|
568
|
+
raise GetoptError("not a directory")
|
|
569
|
+
M = Maildir(maildir_path)
|
|
570
|
+
with Pfx(pop_target):
|
|
571
|
+
try:
|
|
572
|
+
with POP3(pop_target) as pop3:
|
|
573
|
+
msg_uid_map = dict(pop3.client_uidl())
|
|
574
|
+
print(
|
|
575
|
+
f'{len(msg_uid_map)} message',
|
|
576
|
+
('' if len(msg_uid_map) == 1 else 's'),
|
|
577
|
+
('.' if len(msg_uid_map) == 0 else ':'),
|
|
578
|
+
sep=''
|
|
579
|
+
)
|
|
580
|
+
with ResultSet() as deleRs:
|
|
581
|
+
with ResultSet() as retrRs:
|
|
582
|
+
for msg_n in sorted(msg_uid_map):
|
|
583
|
+
runstate.raiseif()
|
|
584
|
+
retrRs.add(pop3.dl_bg(msg_n, M, deleRs if doit else None))
|
|
585
|
+
if once:
|
|
586
|
+
break
|
|
587
|
+
if limit is not None:
|
|
588
|
+
limit -= 1
|
|
589
|
+
if limit <= 0:
|
|
590
|
+
break
|
|
591
|
+
pop3.flush()
|
|
592
|
+
retrRs.wait()
|
|
593
|
+
# now the deleRs are all queued
|
|
594
|
+
pop3.flush()
|
|
595
|
+
if deleRs:
|
|
596
|
+
print("wait for DELEs...")
|
|
597
|
+
deleRs.wait()
|
|
598
|
+
except ConnectionRefusedError as e:
|
|
599
|
+
error("connection refused: %s", e)
|
|
600
|
+
return 1
|
|
601
|
+
|
|
602
|
+
if __name__ == '__main__':
|
|
603
|
+
sys.exit(main(sys.argv))
|