blob: f6e746e7c95c5bfdcaebff155a140de34d71fed1 [file] [log] [blame]
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001"""An NNTP client class based on:
2- RFC 977: Network News Transfer Protocol
3- RFC 2980: Common NNTP Extensions
4- RFC 3977: Network News Transfer Protocol (version 2)
Guido van Rossumc629d341992-11-05 10:43:02 +00005
Guido van Rossum54f22ed2000-02-04 15:10:34 +00006Example:
Guido van Rossumc629d341992-11-05 10:43:02 +00007
Guido van Rossum54f22ed2000-02-04 15:10:34 +00008>>> from nntplib import NNTP
9>>> s = NNTP('news')
10>>> resp, count, first, last, name = s.group('comp.lang.python')
Guido van Rossum7131f842007-02-09 20:13:25 +000011>>> print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Guido van Rossum54f22ed2000-02-04 15:10:34 +000012Group comp.lang.python has 51 articles, range 5770 to 5821
Christian Heimes933238a2008-11-05 19:44:21 +000013>>> resp, subs = s.xhdr('subject', '{0}-{1}'.format(first, last))
Guido van Rossum54f22ed2000-02-04 15:10:34 +000014>>> resp = s.quit()
15>>>
Guido van Rossumc629d341992-11-05 10:43:02 +000016
Guido van Rossum54f22ed2000-02-04 15:10:34 +000017Here 'resp' is the server response line.
18Error responses are turned into exceptions.
19
20To post an article from a file:
Christian Heimes933238a2008-11-05 19:44:21 +000021>>> f = open(filename, 'rb') # file containing article, including header
Guido van Rossum54f22ed2000-02-04 15:10:34 +000022>>> resp = s.post(f)
23>>>
24
25For descriptions of all methods, read the comments in the code below.
26Note that all arguments and return values representing article numbers
27are strings, not numbers, since they are rarely used for calculations.
28"""
29
30# RFC 977 by Brian Kantor and Phil Lapsley.
31# xover, xgtitle, xpath, date methods by Kevan Heydon
Guido van Rossum8421c4e1995-09-22 00:52:38 +000032
Antoine Pitrou69ab9512010-09-29 15:03:40 +000033# Incompatible changes from the 2.x nntplib:
34# - all commands are encoded as UTF-8 data (using the "surrogateescape"
35# error handler), except for raw message data (POST, IHAVE)
36# - all responses are decoded as UTF-8 data (using the "surrogateescape"
37# error handler), except for raw message data (ARTICLE, HEAD, BODY)
38# - the `file` argument to various methods is keyword-only
39#
40# - NNTP.date() returns a datetime object
41# - NNTP.newgroups() and NNTP.newnews() take a datetime (or date) object,
42# rather than a pair of (date, time) strings.
43# - NNTP.newgroups() and NNTP.list() return a list of GroupInfo named tuples
44# - NNTP.descriptions() returns a dict mapping group names to descriptions
45# - NNTP.xover() returns a list of dicts mapping field names (header or metadata)
46# to field values; each dict representing a message overview.
47# - NNTP.article(), NNTP.head() and NNTP.body() return a (response, ArticleInfo)
48# tuple.
49# - the "internal" methods have been marked private (they now start with
50# an underscore)
51
52# Other changes from the 2.x/3.1 nntplib:
53# - automatic querying of capabilities at connect
54# - New method NNTP.getcapabilities()
55# - New method NNTP.over()
56# - New helper function decode_header()
57# - NNTP.post() and NNTP.ihave() accept file objects, bytes-like objects and
58# arbitrary iterables yielding lines.
59# - An extensive test suite :-)
60
61# TODO:
62# - return structured data (GroupInfo etc.) everywhere
63# - support HDR
Guido van Rossumc629d341992-11-05 10:43:02 +000064
65# Imports
Guido van Rossum9694fca1997-10-22 21:00:49 +000066import re
Guido van Rossumc629d341992-11-05 10:43:02 +000067import socket
Antoine Pitrou69ab9512010-09-29 15:03:40 +000068import collections
69import datetime
Steve Dower60419a72019-06-24 08:42:54 -070070import sys
Guido van Rossumc629d341992-11-05 10:43:02 +000071
Antoine Pitrou1cb121e2010-11-09 18:54:37 +000072try:
73 import ssl
Brett Cannoncd171c82013-07-04 17:43:24 -040074except ImportError:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +000075 _have_ssl = False
76else:
77 _have_ssl = True
78
Antoine Pitrou69ab9512010-09-29 15:03:40 +000079from email.header import decode_header as _email_decode_header
80from socket import _GLOBAL_DEFAULT_TIMEOUT
81
82__all__ = ["NNTP",
Berker Peksag96756b62014-09-20 08:53:05 +030083 "NNTPError", "NNTPReplyError", "NNTPTemporaryError",
84 "NNTPPermanentError", "NNTPProtocolError", "NNTPDataError",
Antoine Pitrou69ab9512010-09-29 15:03:40 +000085 "decode_header",
86 ]
Tim Peters2344fae2001-01-15 00:50:52 +000087
Georg Brandl28e78412013-10-27 07:29:47 +010088# maximal line length when calling readline(). This is to prevent
Berker Peksag740c7302014-07-09 20:15:28 +030089# reading arbitrary length lines. RFC 3977 limits NNTP line length to
Georg Brandl28e78412013-10-27 07:29:47 +010090# 512 characters, including CRLF. We have selected 2048 just to be on
91# the safe side.
92_MAXLINE = 2048
93
94
Barry Warsaw9dd78722000-02-10 20:25:53 +000095# Exceptions raised when an error or invalid response is received
96class NNTPError(Exception):
Tim Peters2344fae2001-01-15 00:50:52 +000097 """Base class for all nntplib exceptions"""
98 def __init__(self, *args):
Guido van Rossum68468eb2003-02-27 20:14:51 +000099 Exception.__init__(self, *args)
Tim Peters2344fae2001-01-15 00:50:52 +0000100 try:
101 self.response = args[0]
102 except IndexError:
103 self.response = 'No response given'
Barry Warsaw9dd78722000-02-10 20:25:53 +0000104
105class NNTPReplyError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000106 """Unexpected [123]xx reply"""
107 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000108
109class NNTPTemporaryError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000110 """4xx errors"""
111 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000112
113class NNTPPermanentError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000114 """5xx errors"""
115 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000116
117class NNTPProtocolError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000118 """Response does not begin with [1-5]"""
119 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000120
121class NNTPDataError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000122 """Error in response data"""
123 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000124
Tim Peters2344fae2001-01-15 00:50:52 +0000125
Guido van Rossumc629d341992-11-05 10:43:02 +0000126# Standard port used by NNTP servers
127NNTP_PORT = 119
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000128NNTP_SSL_PORT = 563
Guido van Rossumc629d341992-11-05 10:43:02 +0000129
130# Response numbers that are followed by additional text (e.g. article)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000131_LONGRESP = {
132 '100', # HELP
133 '101', # CAPABILITIES
134 '211', # LISTGROUP (also not multi-line with GROUP)
135 '215', # LIST
136 '220', # ARTICLE
137 '221', # HEAD, XHDR
138 '222', # BODY
139 '224', # OVER, XOVER
140 '225', # HDR
141 '230', # NEWNEWS
142 '231', # NEWGROUPS
143 '282', # XGTITLE
144}
Guido van Rossumc629d341992-11-05 10:43:02 +0000145
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000146# Default decoded value for LIST OVERVIEW.FMT if not supported
147_DEFAULT_OVERVIEW_FMT = [
148 "subject", "from", "date", "message-id", "references", ":bytes", ":lines"]
149
150# Alternative names allowed in LIST OVERVIEW.FMT response
151_OVERVIEW_FMT_ALTERNATIVES = {
152 'bytes': ':bytes',
153 'lines': ':lines',
154}
Guido van Rossumc629d341992-11-05 10:43:02 +0000155
156# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000157_CRLF = b'\r\n'
158
159GroupInfo = collections.namedtuple('GroupInfo',
160 ['group', 'last', 'first', 'flag'])
161
162ArticleInfo = collections.namedtuple('ArticleInfo',
163 ['number', 'message_id', 'lines'])
Guido van Rossumc629d341992-11-05 10:43:02 +0000164
165
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000166# Helper function(s)
167def decode_header(header_str):
Martin Panter6245cb32016-04-15 02:14:19 +0000168 """Takes a unicode string representing a munged header value
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000169 and decodes it as a (possibly non-ASCII) readable value."""
170 parts = []
171 for v, enc in _email_decode_header(header_str):
172 if isinstance(v, bytes):
173 parts.append(v.decode(enc or 'ascii'))
174 else:
175 parts.append(v)
R David Murray07ea53c2012-06-02 17:56:49 -0400176 return ''.join(parts)
Tim Peters2344fae2001-01-15 00:50:52 +0000177
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000178def _parse_overview_fmt(lines):
179 """Parse a list of string representing the response to LIST OVERVIEW.FMT
180 and return a list of header/metadata names.
181 Raises NNTPDataError if the response is not compliant
182 (cf. RFC 3977, section 8.4)."""
183 fmt = []
184 for line in lines:
185 if line[0] == ':':
186 # Metadata name (e.g. ":bytes")
187 name, _, suffix = line[1:].partition(':')
188 name = ':' + name
189 else:
190 # Header name (e.g. "Subject:" or "Xref:full")
191 name, _, suffix = line.partition(':')
192 name = name.lower()
193 name = _OVERVIEW_FMT_ALTERNATIVES.get(name, name)
194 # Should we do something with the suffix?
195 fmt.append(name)
196 defaults = _DEFAULT_OVERVIEW_FMT
197 if len(fmt) < len(defaults):
198 raise NNTPDataError("LIST OVERVIEW.FMT response too short")
199 if fmt[:len(defaults)] != defaults:
200 raise NNTPDataError("LIST OVERVIEW.FMT redefines default fields")
201 return fmt
202
203def _parse_overview(lines, fmt, data_process_func=None):
Martin Panter7462b6492015-11-02 03:37:02 +0000204 """Parse the response to an OVER or XOVER command according to the
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000205 overview format `fmt`."""
206 n_defaults = len(_DEFAULT_OVERVIEW_FMT)
207 overview = []
208 for line in lines:
209 fields = {}
210 article_number, *tokens = line.split('\t')
211 article_number = int(article_number)
212 for i, token in enumerate(tokens):
213 if i >= len(fmt):
214 # XXX should we raise an error? Some servers might not
215 # support LIST OVERVIEW.FMT and still return additional
216 # headers.
217 continue
218 field_name = fmt[i]
219 is_metadata = field_name.startswith(':')
220 if i >= n_defaults and not is_metadata:
221 # Non-default header names are included in full in the response
Antoine Pitrou4103bc02010-11-03 18:18:43 +0000222 # (unless the field is totally empty)
223 h = field_name + ": "
224 if token and token[:len(h)].lower() != h:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000225 raise NNTPDataError("OVER/XOVER response doesn't include "
226 "names of additional headers")
Antoine Pitrou4103bc02010-11-03 18:18:43 +0000227 token = token[len(h):] if token else None
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000228 fields[fmt[i]] = token
229 overview.append((article_number, fields))
230 return overview
231
232def _parse_datetime(date_str, time_str=None):
233 """Parse a pair of (date, time) strings, and return a datetime object.
234 If only the date is given, it is assumed to be date and time
235 concatenated together (e.g. response to the DATE command).
236 """
237 if time_str is None:
238 time_str = date_str[-6:]
239 date_str = date_str[:-6]
240 hours = int(time_str[:2])
241 minutes = int(time_str[2:4])
242 seconds = int(time_str[4:])
243 year = int(date_str[:-4])
244 month = int(date_str[-4:-2])
245 day = int(date_str[-2:])
246 # RFC 3977 doesn't say how to interpret 2-char years. Assume that
247 # there are no dates before 1970 on Usenet.
248 if year < 70:
249 year += 2000
250 elif year < 100:
251 year += 1900
252 return datetime.datetime(year, month, day, hours, minutes, seconds)
253
254def _unparse_datetime(dt, legacy=False):
255 """Format a date or datetime object as a pair of (date, time) strings
256 in the format required by the NEWNEWS and NEWGROUPS commands. If a
257 date object is passed, the time is assumed to be midnight (00h00).
258
259 The returned representation depends on the legacy flag:
260 * if legacy is False (the default):
261 date has the YYYYMMDD format and time the HHMMSS format
262 * if legacy is True:
263 date has the YYMMDD format and time the HHMMSS format.
264 RFC 3977 compliant servers should understand both formats; therefore,
265 legacy is only needed when talking to old servers.
266 """
267 if not isinstance(dt, datetime.datetime):
268 time_str = "000000"
269 else:
270 time_str = "{0.hour:02d}{0.minute:02d}{0.second:02d}".format(dt)
271 y = dt.year
272 if legacy:
273 y = y % 100
274 date_str = "{0:02d}{1.month:02d}{1.day:02d}".format(y, dt)
275 else:
276 date_str = "{0:04d}{1.month:02d}{1.day:02d}".format(y, dt)
277 return date_str, time_str
278
279
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000280if _have_ssl:
281
Christian Heimes216d4632013-12-02 20:20:11 +0100282 def _encrypt_on(sock, context, hostname):
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000283 """Wrap a socket in SSL/TLS. Arguments:
284 - sock: Socket to wrap
285 - context: SSL context to use for the encrypted connection
286 Returns:
287 - sock: New, encrypted socket.
288 """
289 # Generate a default SSL context if none was passed.
290 if context is None:
Christian Heimes67986f92013-11-23 22:43:47 +0100291 context = ssl._create_stdlib_context()
Benjamin Peterson7243b572014-11-23 17:04:34 -0600292 return context.wrap_socket(sock, server_hostname=hostname)
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000293
294
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000295# The classes themselves
Dong-hee Naaa92a7c2020-05-16 19:31:54 +0900296class NNTP:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000297 # UTF-8 is the character set for all NNTP commands and responses: they
298 # are automatically encoded (when sending) and decoded (and receiving)
299 # by this class.
300 # However, some multi-line data blocks can contain arbitrary bytes (for
301 # example, latin-1 or utf-16 data in the body of a message). Commands
302 # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
303 # data will therefore only accept and produce bytes objects.
304 # Furthermore, since there could be non-compliant servers out there,
305 # we use 'surrogateescape' as the error handler for fault tolerance
306 # and easy round-tripping. This could be useful for some applications
307 # (e.g. NNTP gateways).
308
309 encoding = 'utf-8'
310 errors = 'surrogateescape'
311
Dong-hee Naaa92a7c2020-05-16 19:31:54 +0900312 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
313 readermode=None, usenetrc=False,
314 timeout=_GLOBAL_DEFAULT_TIMEOUT):
Tim Peters2344fae2001-01-15 00:50:52 +0000315 """Initialize an instance. Arguments:
Dong-hee Naaa92a7c2020-05-16 19:31:54 +0900316 - host: hostname to connect to
317 - port: port to connect to (default the standard NNTP port)
318 - user: username to authenticate with
319 - password: password to use with username
Tim Peters2344fae2001-01-15 00:50:52 +0000320 - readermode: if true, send 'mode reader' command after
321 connecting.
Dong-hee Naaa92a7c2020-05-16 19:31:54 +0900322 - usenetrc: allow loading username and password from ~/.netrc file
323 if not specified explicitly
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000324 - timeout: timeout (in seconds) used for socket connections
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000325
Tim Peters2344fae2001-01-15 00:50:52 +0000326 readermode is sometimes necessary if you are connecting to an
327 NNTP server on the local machine and intend to call
Ezio Melotti42da6632011-03-15 05:18:48 +0200328 reader-specific commands, such as `group'. If you get
Tim Peters2344fae2001-01-15 00:50:52 +0000329 unexpected NNTPPermanentErrors, you might need to set
330 readermode.
331 """
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000332 self.host = host
Dong-hee Naaa92a7c2020-05-16 19:31:54 +0900333 self.port = port
334 self.sock = self._create_socket(timeout)
335 self.file = None
336 try:
337 self.file = self.sock.makefile("rwb")
338 self._base_init(readermode)
339 if user or usenetrc:
340 self.login(user, password, usenetrc)
341 except:
342 if self.file:
343 self.file.close()
344 self.sock.close()
345 raise
346
347 def _base_init(self, readermode):
348 """Partial initialization for the NNTP protocol.
349 This instance method is extracted for supporting the test code.
350 """
Tim Peters2344fae2001-01-15 00:50:52 +0000351 self.debugging = 0
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000352 self.welcome = self._getresp()
Tim Petersdfb673b2001-01-16 07:12:46 +0000353
Antoine Pitrou71135622012-02-14 23:29:34 +0100354 # Inquire about capabilities (RFC 3977).
355 self._caps = None
356 self.getcapabilities()
357
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000358 # 'MODE READER' is sometimes necessary to enable 'reader' mode.
359 # However, the order in which 'MODE READER' and 'AUTHINFO' need to
360 # arrive differs between some NNTP servers. If _setreadermode() fails
361 # with an authorization failed error, it will set this to True;
362 # the login() routine will interpret that as a request to try again
363 # after performing its normal function.
Antoine Pitrou71135622012-02-14 23:29:34 +0100364 # Enable only if we're not already in READER mode anyway.
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000365 self.readermode_afterauth = False
Antoine Pitrou71135622012-02-14 23:29:34 +0100366 if readermode and 'READER' not in self._caps:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000367 self._setreadermode()
Antoine Pitrou71135622012-02-14 23:29:34 +0100368 if not self.readermode_afterauth:
369 # Capabilities might have changed after MODE READER
370 self._caps = None
371 self.getcapabilities()
Tim Petersdfb673b2001-01-16 07:12:46 +0000372
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000373 # RFC 4642 2.2.2: Both the client and the server MUST know if there is
374 # a TLS session active. A client MUST NOT attempt to start a TLS
375 # session if a TLS session is already active.
376 self.tls_on = False
377
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000378 # Log in and encryption setup order is left to subclasses.
379 self.authenticated = False
Guido van Rossumc629d341992-11-05 10:43:02 +0000380
Giampaolo Rodolà424298a2011-03-03 18:34:06 +0000381 def __enter__(self):
382 return self
383
384 def __exit__(self, *args):
385 is_connected = lambda: hasattr(self, "file")
386 if is_connected():
387 try:
388 self.quit()
Andrew Svetlov0832af62012-12-18 23:10:48 +0200389 except (OSError, EOFError):
Giampaolo Rodolà424298a2011-03-03 18:34:06 +0000390 pass
391 finally:
392 if is_connected():
393 self._close()
394
Dong-hee Naaa92a7c2020-05-16 19:31:54 +0900395 def _create_socket(self, timeout):
396 if timeout is not None and not timeout:
397 raise ValueError('Non-blocking socket (timeout=0) is not supported')
398 sys.audit("nntplib.connect", self, self.host, self.port)
399 return socket.create_connection((self.host, self.port), timeout)
400
Tim Peters2344fae2001-01-15 00:50:52 +0000401 def getwelcome(self):
402 """Get the welcome message from the server
403 (this is read and squirreled away by __init__()).
404 If the response code is 200, posting is allowed;
405 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000406
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000407 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000408 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000409
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000410 def getcapabilities(self):
411 """Get the server capabilities, as read by __init__().
412 If the CAPABILITIES command is not supported, an empty dict is
413 returned."""
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000414 if self._caps is None:
415 self.nntp_version = 1
416 self.nntp_implementation = None
417 try:
418 resp, caps = self.capabilities()
Antoine Pitrou54411c12012-02-12 19:14:17 +0100419 except (NNTPPermanentError, NNTPTemporaryError):
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000420 # Server doesn't support capabilities
421 self._caps = {}
422 else:
423 self._caps = caps
424 if 'VERSION' in caps:
425 # The server can advertise several supported versions,
426 # choose the highest.
427 self.nntp_version = max(map(int, caps['VERSION']))
428 if 'IMPLEMENTATION' in caps:
429 self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000430 return self._caps
431
Tim Peters2344fae2001-01-15 00:50:52 +0000432 def set_debuglevel(self, level):
433 """Set the debugging level. Argument 'level' means:
434 0: no debugging output (default)
435 1: print commands and responses but not body text etc.
436 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000437
Tim Peters2344fae2001-01-15 00:50:52 +0000438 self.debugging = level
439 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000440
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000441 def _putline(self, line):
442 """Internal: send one line to the server, appending CRLF.
443 The `line` must be a bytes-like object."""
Steve Dower44f91c32019-06-27 10:47:59 -0700444 sys.audit("nntplib.putline", self, line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000445 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000446 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000447 self.file.write(line)
448 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000449
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000450 def _putcmd(self, line):
451 """Internal: send one command to the server (through _putline()).
Martin Panter6245cb32016-04-15 02:14:19 +0000452 The `line` must be a unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000453 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000454 line = line.encode(self.encoding, self.errors)
455 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000456
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000457 def _getline(self, strip_crlf=True):
458 """Internal: return one line from the server, stripping _CRLF.
459 Raise EOFError if the connection is closed.
460 Returns a bytes object."""
Georg Brandl28e78412013-10-27 07:29:47 +0100461 line = self.file.readline(_MAXLINE +1)
462 if len(line) > _MAXLINE:
463 raise NNTPDataError('line too long')
Tim Peters2344fae2001-01-15 00:50:52 +0000464 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000465 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000466 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000467 if strip_crlf:
468 if line[-2:] == _CRLF:
469 line = line[:-2]
470 elif line[-1:] in _CRLF:
471 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000472 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000473
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000474 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000475 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000476 Raise various errors if the response indicates an error.
Martin Panter6245cb32016-04-15 02:14:19 +0000477 Returns a unicode string."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000478 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000479 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000480 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000481 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000482 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000483 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000484 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000485 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000486 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000487 raise NNTPProtocolError(resp)
488 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000489
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000490 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000491 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000492 Raise various errors if the response indicates an error.
493
Martin Panter6245cb32016-04-15 02:14:19 +0000494 Returns a (response, lines) tuple where `response` is a unicode
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000495 string and `lines` is a list of bytes objects.
496 If `file` is a file-like object, it must be open in binary mode.
497 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000498
499 openedFile = None
500 try:
501 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000502 if isinstance(file, (str, bytes)):
503 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000504
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000505 resp = self._getresp()
506 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000507 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000508
509 lines = []
510 if file is not None:
511 # XXX lines = None instead?
512 terminators = (b'.' + _CRLF, b'.\n')
513 while 1:
514 line = self._getline(False)
515 if line in terminators:
516 break
517 if line.startswith(b'..'):
518 line = line[1:]
519 file.write(line)
520 else:
521 terminator = b'.'
522 while 1:
523 line = self._getline()
524 if line == terminator:
525 break
526 if line.startswith(b'..'):
527 line = line[1:]
528 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000529 finally:
530 # If this method created the file, then it must close it
531 if openedFile:
532 openedFile.close()
533
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000534 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000535
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000536 def _shortcmd(self, line):
537 """Internal: send a command and get the response.
538 Same return value as _getresp()."""
539 self._putcmd(line)
540 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000541
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000542 def _longcmd(self, line, file=None):
543 """Internal: send a command and get the response plus following text.
544 Same return value as _getlongresp()."""
545 self._putcmd(line)
546 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000547
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000548 def _longcmdstring(self, line, file=None):
549 """Internal: send a command and get the response plus following text.
550 Same as _longcmd() and _getlongresp(), except that the returned `lines`
551 are unicode strings rather than bytes objects.
552 """
553 self._putcmd(line)
554 resp, list = self._getlongresp(file)
555 return resp, [line.decode(self.encoding, self.errors)
556 for line in list]
557
558 def _getoverviewfmt(self):
559 """Internal: get the overview format. Queries the server if not
560 already done, else returns the cached value."""
561 try:
562 return self._cachedoverviewfmt
563 except AttributeError:
564 pass
565 try:
566 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
567 except NNTPPermanentError:
568 # Not supported by server?
569 fmt = _DEFAULT_OVERVIEW_FMT[:]
570 else:
571 fmt = _parse_overview_fmt(lines)
572 self._cachedoverviewfmt = fmt
573 return fmt
574
575 def _grouplist(self, lines):
576 # Parse lines into "group last first flag"
577 return [GroupInfo(*line.split()) for line in lines]
578
579 def capabilities(self):
580 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000581 Return:
582 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000583 - caps: a dictionary mapping capability names to lists of tokens
584 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
585 """
586 caps = {}
587 resp, lines = self._longcmdstring("CAPABILITIES")
588 for line in lines:
589 name, *tokens = line.split()
590 caps[name] = tokens
591 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000592
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000593 def newgroups(self, date, *, file=None):
594 """Process a NEWGROUPS command. Arguments:
595 - date: a date or datetime object
596 Return:
597 - resp: server response if successful
598 - list: list of newsgroup names
599 """
600 if not isinstance(date, (datetime.date, datetime.date)):
601 raise TypeError(
602 "the date parameter must be a date or datetime object, "
603 "not '{:40}'".format(date.__class__.__name__))
604 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
605 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
606 resp, lines = self._longcmdstring(cmd, file)
607 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000608
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000609 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000610 """Process a NEWNEWS command. Arguments:
611 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000612 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000613 Return:
614 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000615 - list: list of message ids
616 """
617 if not isinstance(date, (datetime.date, datetime.date)):
618 raise TypeError(
619 "the date parameter must be a date or datetime object, "
620 "not '{:40}'".format(date.__class__.__name__))
621 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
622 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
623 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000624
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000625 def list(self, group_pattern=None, *, file=None):
626 """Process a LIST or LIST ACTIVE command. Arguments:
627 - group_pattern: a pattern indicating which groups to query
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000628 - file: Filename string or file object to store the result in
629 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000630 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000631 - list: list of (group, last, first, flag) (strings)
632 """
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000633 if group_pattern is not None:
634 command = 'LIST ACTIVE ' + group_pattern
635 else:
636 command = 'LIST'
637 resp, lines = self._longcmdstring(command, file)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000638 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000639
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000640 def _getdescriptions(self, group_pattern, return_all):
641 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
642 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
643 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
644 if not resp.startswith('215'):
645 # Now the deprecated XGTITLE. This either raises an error
646 # or succeeds with the same output structure as LIST
647 # NEWSGROUPS.
648 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
649 groups = {}
650 for raw_line in lines:
651 match = line_pat.search(raw_line.strip())
652 if match:
653 name, desc = match.group(1, 2)
654 if not return_all:
655 return desc
656 groups[name] = desc
657 if return_all:
658 return resp, groups
659 else:
660 # Nothing found
661 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000662
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000663 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000664 """Get a description for a single group. If more than one
665 group matches ('group' is a pattern), return the first. If no
666 group matches, return an empty string.
667
668 This elides the response code from the server, since it can
669 only be '215' or '285' (for xgtitle) anyway. If the response
670 code is needed, use the 'descriptions' method.
671
672 NOTE: This neither checks for a wildcard in 'group' nor does
673 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000674 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000675
676 def descriptions(self, group_pattern):
677 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000678 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000679
Tim Peters2344fae2001-01-15 00:50:52 +0000680 def group(self, name):
681 """Process a GROUP command. Argument:
682 - group: the group name
683 Returns:
684 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000685 - count: number of articles
686 - first: first article number
687 - last: last article number
688 - name: the group name
689 """
690 resp = self._shortcmd('GROUP ' + name)
691 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000692 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000693 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000694 count = first = last = 0
695 n = len(words)
696 if n > 1:
697 count = words[1]
698 if n > 2:
699 first = words[2]
700 if n > 3:
701 last = words[3]
702 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000703 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000704 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000705
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000706 def help(self, *, file=None):
707 """Process a HELP command. Argument:
708 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000709 Returns:
710 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000711 - list: list of strings returned by the server in response to the
712 HELP command
713 """
714 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000715
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000716 def _statparse(self, resp):
717 """Internal: parse the response line of a STAT, NEXT, LAST,
718 ARTICLE, HEAD or BODY command."""
719 if not resp.startswith('22'):
720 raise NNTPReplyError(resp)
721 words = resp.split()
722 art_num = int(words[1])
723 message_id = words[2]
724 return resp, art_num, message_id
725
726 def _statcmd(self, line):
727 """Internal: process a STAT, NEXT or LAST command."""
728 resp = self._shortcmd(line)
729 return self._statparse(resp)
730
731 def stat(self, message_spec=None):
732 """Process a STAT command. Argument:
733 - message_spec: article number or message id (if not specified,
734 the current article is selected)
735 Returns:
736 - resp: server response if successful
737 - art_num: the article number
738 - message_id: the message id
739 """
740 if message_spec:
741 return self._statcmd('STAT {0}'.format(message_spec))
742 else:
743 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000744
Tim Peters2344fae2001-01-15 00:50:52 +0000745 def next(self):
746 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000747 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000748
Tim Peters2344fae2001-01-15 00:50:52 +0000749 def last(self):
750 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000751 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000752
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000753 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000754 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000755 resp, lines = self._longcmd(line, file)
756 resp, art_num, message_id = self._statparse(resp)
757 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000758
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000759 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000760 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000761 - message_spec: article number or message id
762 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000763 Returns:
764 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000765 - ArticleInfo: (article number, message id, list of header lines)
766 """
767 if message_spec is not None:
768 cmd = 'HEAD {0}'.format(message_spec)
769 else:
770 cmd = 'HEAD'
771 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000772
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000773 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000774 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000775 - message_spec: article number or message id
776 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000777 Returns:
778 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000779 - ArticleInfo: (article number, message id, list of body lines)
780 """
781 if message_spec is not None:
782 cmd = 'BODY {0}'.format(message_spec)
783 else:
784 cmd = 'BODY'
785 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000786
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000787 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000788 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000789 - message_spec: article number or message id
790 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000791 Returns:
792 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000793 - ArticleInfo: (article number, message id, list of article lines)
794 """
795 if message_spec is not None:
796 cmd = 'ARTICLE {0}'.format(message_spec)
797 else:
798 cmd = 'ARTICLE'
799 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000800
Tim Peters2344fae2001-01-15 00:50:52 +0000801 def slave(self):
802 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000803 - resp: server response if successful
804 """
805 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000806
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000807 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000808 """Process an XHDR command (optional server extension). Arguments:
809 - hdr: the header type (e.g. 'subject')
810 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000811 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000812 Returns:
813 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000814 - list: list of (nr, value) strings
815 """
816 pat = re.compile('^([0-9]+) ?(.*)\n?')
817 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
818 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000819 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000820 return m.group(1, 2) if m else line
821 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000822
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000823 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000824 """Process an XOVER command (optional server extension) Arguments:
825 - start: start of range
826 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000827 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000828 Returns:
829 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000830 - list: list of dicts containing the response fields
831 """
832 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
833 file)
834 fmt = self._getoverviewfmt()
835 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000836
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000837 def over(self, message_spec, *, file=None):
838 """Process an OVER command. If the command isn't supported, fall
839 back to XOVER. Arguments:
840 - message_spec:
841 - either a message id, indicating the article to fetch
842 information about
843 - or a (start, end) tuple, indicating a range of article numbers;
844 if end is None, information up to the newest message will be
845 retrieved
846 - or None, indicating the current article number must be used
847 - file: Filename string or file object to store the result in
848 Returns:
849 - resp: server response if successful
850 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000851
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000852 NOTE: the "message id" form isn't supported by XOVER
853 """
854 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
855 if isinstance(message_spec, (tuple, list)):
856 start, end = message_spec
857 cmd += ' {0}-{1}'.format(start, end or '')
858 elif message_spec is not None:
859 cmd = cmd + ' ' + message_spec
860 resp, lines = self._longcmdstring(cmd, file)
861 fmt = self._getoverviewfmt()
862 return resp, _parse_overview(lines, fmt)
863
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000864 def date(self):
865 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000866 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000867 - resp: server response if successful
868 - date: datetime object
869 """
870 resp = self._shortcmd("DATE")
871 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000872 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000873 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000874 if len(elem) != 2:
875 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000876 date = elem[1]
877 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000878 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000879 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000880
Christian Heimes933238a2008-11-05 19:44:21 +0000881 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000882 resp = self._shortcmd(command)
883 # Raises a specific exception if posting is not allowed
884 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000885 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000886 if isinstance(f, (bytes, bytearray)):
887 f = f.splitlines()
888 # We don't use _putline() because:
889 # - we don't want additional CRLF if the file or iterable is already
890 # in the right format
891 # - we don't want a spurious flush() after each line is written
892 for line in f:
893 if not line.endswith(_CRLF):
894 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000895 if line.startswith(b'.'):
896 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000897 self.file.write(line)
898 self.file.write(b".\r\n")
899 self.file.flush()
900 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000901
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000902 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000903 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000904 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000905 Returns:
906 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000907 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000908
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000909 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000910 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000911 - message_id: message-id of the article
912 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000913 Returns:
914 - resp: server response if successful
915 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000916 return self._post('IHAVE {0}'.format(message_id), data)
917
918 def _close(self):
Dong-hee Naaa92a7c2020-05-16 19:31:54 +0900919 try:
920 if self.file:
921 self.file.close()
922 del self.file
923 finally:
924 self.sock.close()
Guido van Rossumc629d341992-11-05 10:43:02 +0000925
Tim Peters2344fae2001-01-15 00:50:52 +0000926 def quit(self):
927 """Process a QUIT command and close the socket. Returns:
928 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000929 try:
930 resp = self._shortcmd('QUIT')
931 finally:
932 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000933 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000934
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000935 def login(self, user=None, password=None, usenetrc=True):
936 if self.authenticated:
937 raise ValueError("Already logged in.")
938 if not user and not usenetrc:
939 raise ValueError(
940 "At least one of `user` and `usenetrc` must be specified")
941 # If no login/password was specified but netrc was requested,
942 # try to get them from ~/.netrc
943 # Presume that if .netrc has an entry, NNRP authentication is required.
944 try:
945 if usenetrc and not user:
946 import netrc
947 credentials = netrc.netrc()
948 auth = credentials.authenticators(self.host)
949 if auth:
950 user = auth[0]
951 password = auth[2]
Andrew Svetlovf7a17b42012-12-25 16:47:37 +0200952 except OSError:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000953 pass
954 # Perform NNTP authentication if needed.
955 if not user:
956 return
957 resp = self._shortcmd('authinfo user ' + user)
958 if resp.startswith('381'):
959 if not password:
960 raise NNTPReplyError(resp)
961 else:
962 resp = self._shortcmd('authinfo pass ' + password)
963 if not resp.startswith('281'):
964 raise NNTPPermanentError(resp)
Antoine Pitrou54411c12012-02-12 19:14:17 +0100965 # Capabilities might have changed after login
966 self._caps = None
967 self.getcapabilities()
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000968 # Attempt to send mode reader if it was requested after login.
Antoine Pitrou71135622012-02-14 23:29:34 +0100969 # Only do so if we're not in reader mode already.
970 if self.readermode_afterauth and 'READER' not in self._caps:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000971 self._setreadermode()
Antoine Pitrou71135622012-02-14 23:29:34 +0100972 # Capabilities might have changed after MODE READER
973 self._caps = None
974 self.getcapabilities()
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000975
976 def _setreadermode(self):
977 try:
978 self.welcome = self._shortcmd('mode reader')
979 except NNTPPermanentError:
980 # Error 5xx, probably 'not implemented'
981 pass
982 except NNTPTemporaryError as e:
983 if e.response.startswith('480'):
984 # Need authorization before 'mode reader'
985 self.readermode_afterauth = True
986 else:
987 raise
988
989 if _have_ssl:
990 def starttls(self, context=None):
991 """Process a STARTTLS command. Arguments:
992 - context: SSL context to use for the encrypted connection
993 """
994 # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if
995 # a TLS session already exists.
996 if self.tls_on:
997 raise ValueError("TLS is already enabled.")
998 if self.authenticated:
999 raise ValueError("TLS cannot be started after authentication.")
1000 resp = self._shortcmd('STARTTLS')
1001 if resp.startswith('382'):
1002 self.file.close()
Christian Heimes216d4632013-12-02 20:20:11 +01001003 self.sock = _encrypt_on(self.sock, context, self.host)
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001004 self.file = self.sock.makefile("rwb")
1005 self.tls_on = True
1006 # Capabilities may change after TLS starts up, so ask for them
1007 # again.
1008 self._caps = None
1009 self.getcapabilities()
1010 else:
1011 raise NNTPError("TLS failed to start.")
1012
Guido van Rossume2ed9df1997-08-26 23:26:18 +00001013
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001014if _have_ssl:
Dong-hee Na5d978a22020-01-12 00:07:36 +09001015 class NNTP_SSL(NNTP):
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001016
1017 def __init__(self, host, port=NNTP_SSL_PORT,
1018 user=None, password=None, ssl_context=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001019 readermode=None, usenetrc=False,
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001020 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1021 """This works identically to NNTP.__init__, except for the change
1022 in default port and the `ssl_context` argument for SSL connections.
1023 """
Dong-hee Na5d978a22020-01-12 00:07:36 +09001024 self.ssl_context = ssl_context
1025 super().__init__(host, port, user, password, readermode,
1026 usenetrc, timeout)
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001027
Dong-hee Na5d978a22020-01-12 00:07:36 +09001028 def _create_socket(self, timeout):
1029 sock = super()._create_socket(timeout)
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001030 try:
Dong-hee Na5d978a22020-01-12 00:07:36 +09001031 sock = _encrypt_on(sock, self.ssl_context, self.host)
1032 except:
1033 sock.close()
1034 raise
1035 else:
1036 return sock
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001037
1038 __all__.append("NNTP_SSL")
1039
1040
Neal Norwitzef679562002-11-14 02:19:44 +00001041# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +00001042if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001043 import argparse
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001044
1045 parser = argparse.ArgumentParser(description="""\
1046 nntplib built-in demo - display the latest articles in a newsgroup""")
1047 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
1048 help='group to fetch messages from (default: %(default)s)')
Dong-hee Na2e6a8ef2020-01-09 00:29:34 +09001049 parser.add_argument('-s', '--server', default='news.gmane.io',
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001050 help='NNTP server hostname (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001051 parser.add_argument('-p', '--port', default=-1, type=int,
1052 help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT))
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001053 parser.add_argument('-n', '--nb-articles', default=10, type=int,
1054 help='number of articles to fetch (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001055 parser.add_argument('-S', '--ssl', action='store_true', default=False,
1056 help='use NNTP over SSL')
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001057 args = parser.parse_args()
1058
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001059 port = args.port
1060 if not args.ssl:
1061 if port == -1:
1062 port = NNTP_PORT
1063 s = NNTP(host=args.server, port=port)
1064 else:
1065 if port == -1:
1066 port = NNTP_SSL_PORT
1067 s = NNTP_SSL(host=args.server, port=port)
1068
1069 caps = s.getcapabilities()
1070 if 'STARTTLS' in caps:
1071 s.starttls()
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001072 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001073 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001074
1075 def cut(s, lim):
1076 if len(s) > lim:
1077 s = s[:lim - 4] + "..."
1078 return s
1079
1080 first = str(int(last) - args.nb_articles + 1)
1081 resp, overviews = s.xover(first, last)
1082 for artnum, over in overviews:
1083 author = decode_header(over['from']).split('<', 1)[0]
1084 subject = decode_header(over['subject'])
1085 lines = int(over[':lines'])
1086 print("{:7} {:20} {:42} ({})".format(
1087 artnum, cut(author, 20), cut(subject, 42), lines)
1088 )
1089
1090 s.quit()