blob: fcb01d319b49ea0afe298ba08fdec275f603e7c4 [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
70import warnings
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",
83 "NNTPReplyError", "NNTPTemporaryError", "NNTPPermanentError",
84 "NNTPProtocolError", "NNTPDataError",
85 "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
89# reading arbitrary lenght lines. RFC 3977 limits NNTP line length to
90# 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):
168 """Takes an unicode string representing a munged header value
169 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):
204 """Parse the response to a OVER or XOVER command according to the
205 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()
Christian Heimes216d4632013-12-02 20:20:11 +0100292 server_hostname = hostname if ssl.HAS_SNI else None
293 return context.wrap_socket(sock, server_hostname=server_hostname)
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000294
295
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000296# The classes themselves
297class _NNTPBase:
298 # UTF-8 is the character set for all NNTP commands and responses: they
299 # are automatically encoded (when sending) and decoded (and receiving)
300 # by this class.
301 # However, some multi-line data blocks can contain arbitrary bytes (for
302 # example, latin-1 or utf-16 data in the body of a message). Commands
303 # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
304 # data will therefore only accept and produce bytes objects.
305 # Furthermore, since there could be non-compliant servers out there,
306 # we use 'surrogateescape' as the error handler for fault tolerance
307 # and easy round-tripping. This could be useful for some applications
308 # (e.g. NNTP gateways).
309
310 encoding = 'utf-8'
311 errors = 'surrogateescape'
312
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000313 def __init__(self, file, host,
314 readermode=None, timeout=_GLOBAL_DEFAULT_TIMEOUT):
Tim Peters2344fae2001-01-15 00:50:52 +0000315 """Initialize an instance. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000316 - file: file-like object (open for read/write in binary mode)
Antoine Pitrou859c4ef2010-11-09 18:58:42 +0000317 - host: hostname of the server
Tim Peters2344fae2001-01-15 00:50:52 +0000318 - readermode: if true, send 'mode reader' command after
319 connecting.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000320 - timeout: timeout (in seconds) used for socket connections
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000321
Tim Peters2344fae2001-01-15 00:50:52 +0000322 readermode is sometimes necessary if you are connecting to an
323 NNTP server on the local machine and intend to call
Ezio Melotti42da6632011-03-15 05:18:48 +0200324 reader-specific commands, such as `group'. If you get
Tim Peters2344fae2001-01-15 00:50:52 +0000325 unexpected NNTPPermanentErrors, you might need to set
326 readermode.
327 """
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000328 self.host = host
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000329 self.file = file
Tim Peters2344fae2001-01-15 00:50:52 +0000330 self.debugging = 0
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000331 self.welcome = self._getresp()
Tim Petersdfb673b2001-01-16 07:12:46 +0000332
Antoine Pitrou71135622012-02-14 23:29:34 +0100333 # Inquire about capabilities (RFC 3977).
334 self._caps = None
335 self.getcapabilities()
336
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000337 # 'MODE READER' is sometimes necessary to enable 'reader' mode.
338 # However, the order in which 'MODE READER' and 'AUTHINFO' need to
339 # arrive differs between some NNTP servers. If _setreadermode() fails
340 # with an authorization failed error, it will set this to True;
341 # the login() routine will interpret that as a request to try again
342 # after performing its normal function.
Antoine Pitrou71135622012-02-14 23:29:34 +0100343 # Enable only if we're not already in READER mode anyway.
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000344 self.readermode_afterauth = False
Antoine Pitrou71135622012-02-14 23:29:34 +0100345 if readermode and 'READER' not in self._caps:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000346 self._setreadermode()
Antoine Pitrou71135622012-02-14 23:29:34 +0100347 if not self.readermode_afterauth:
348 # Capabilities might have changed after MODE READER
349 self._caps = None
350 self.getcapabilities()
Tim Petersdfb673b2001-01-16 07:12:46 +0000351
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000352 # RFC 4642 2.2.2: Both the client and the server MUST know if there is
353 # a TLS session active. A client MUST NOT attempt to start a TLS
354 # session if a TLS session is already active.
355 self.tls_on = False
356
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000357 # Log in and encryption setup order is left to subclasses.
358 self.authenticated = False
Guido van Rossumc629d341992-11-05 10:43:02 +0000359
Giampaolo Rodolà424298a2011-03-03 18:34:06 +0000360 def __enter__(self):
361 return self
362
363 def __exit__(self, *args):
364 is_connected = lambda: hasattr(self, "file")
365 if is_connected():
366 try:
367 self.quit()
Andrew Svetlov0832af62012-12-18 23:10:48 +0200368 except (OSError, EOFError):
Giampaolo Rodolà424298a2011-03-03 18:34:06 +0000369 pass
370 finally:
371 if is_connected():
372 self._close()
373
Tim Peters2344fae2001-01-15 00:50:52 +0000374 def getwelcome(self):
375 """Get the welcome message from the server
376 (this is read and squirreled away by __init__()).
377 If the response code is 200, posting is allowed;
378 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000379
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000380 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000381 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000382
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000383 def getcapabilities(self):
384 """Get the server capabilities, as read by __init__().
385 If the CAPABILITIES command is not supported, an empty dict is
386 returned."""
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000387 if self._caps is None:
388 self.nntp_version = 1
389 self.nntp_implementation = None
390 try:
391 resp, caps = self.capabilities()
Antoine Pitrou54411c12012-02-12 19:14:17 +0100392 except (NNTPPermanentError, NNTPTemporaryError):
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000393 # Server doesn't support capabilities
394 self._caps = {}
395 else:
396 self._caps = caps
397 if 'VERSION' in caps:
398 # The server can advertise several supported versions,
399 # choose the highest.
400 self.nntp_version = max(map(int, caps['VERSION']))
401 if 'IMPLEMENTATION' in caps:
402 self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000403 return self._caps
404
Tim Peters2344fae2001-01-15 00:50:52 +0000405 def set_debuglevel(self, level):
406 """Set the debugging level. Argument 'level' means:
407 0: no debugging output (default)
408 1: print commands and responses but not body text etc.
409 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000410
Tim Peters2344fae2001-01-15 00:50:52 +0000411 self.debugging = level
412 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000413
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000414 def _putline(self, line):
415 """Internal: send one line to the server, appending CRLF.
416 The `line` must be a bytes-like object."""
417 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000418 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000419 self.file.write(line)
420 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000421
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000422 def _putcmd(self, line):
423 """Internal: send one command to the server (through _putline()).
424 The `line` must be an unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000425 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000426 line = line.encode(self.encoding, self.errors)
427 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000428
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000429 def _getline(self, strip_crlf=True):
430 """Internal: return one line from the server, stripping _CRLF.
431 Raise EOFError if the connection is closed.
432 Returns a bytes object."""
Georg Brandl28e78412013-10-27 07:29:47 +0100433 line = self.file.readline(_MAXLINE +1)
434 if len(line) > _MAXLINE:
435 raise NNTPDataError('line too long')
Tim Peters2344fae2001-01-15 00:50:52 +0000436 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000437 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000438 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000439 if strip_crlf:
440 if line[-2:] == _CRLF:
441 line = line[:-2]
442 elif line[-1:] in _CRLF:
443 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000444 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000445
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000446 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000447 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000448 Raise various errors if the response indicates an error.
449 Returns an unicode string."""
450 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000451 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000452 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000453 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000454 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000455 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000456 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000457 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000458 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000459 raise NNTPProtocolError(resp)
460 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000461
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000462 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000463 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000464 Raise various errors if the response indicates an error.
465
466 Returns a (response, lines) tuple where `response` is an unicode
467 string and `lines` is a list of bytes objects.
468 If `file` is a file-like object, it must be open in binary mode.
469 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000470
471 openedFile = None
472 try:
473 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000474 if isinstance(file, (str, bytes)):
475 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000476
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000477 resp = self._getresp()
478 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000479 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000480
481 lines = []
482 if file is not None:
483 # XXX lines = None instead?
484 terminators = (b'.' + _CRLF, b'.\n')
485 while 1:
486 line = self._getline(False)
487 if line in terminators:
488 break
489 if line.startswith(b'..'):
490 line = line[1:]
491 file.write(line)
492 else:
493 terminator = b'.'
494 while 1:
495 line = self._getline()
496 if line == terminator:
497 break
498 if line.startswith(b'..'):
499 line = line[1:]
500 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000501 finally:
502 # If this method created the file, then it must close it
503 if openedFile:
504 openedFile.close()
505
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000506 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000507
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000508 def _shortcmd(self, line):
509 """Internal: send a command and get the response.
510 Same return value as _getresp()."""
511 self._putcmd(line)
512 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000513
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000514 def _longcmd(self, line, file=None):
515 """Internal: send a command and get the response plus following text.
516 Same return value as _getlongresp()."""
517 self._putcmd(line)
518 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000519
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000520 def _longcmdstring(self, line, file=None):
521 """Internal: send a command and get the response plus following text.
522 Same as _longcmd() and _getlongresp(), except that the returned `lines`
523 are unicode strings rather than bytes objects.
524 """
525 self._putcmd(line)
526 resp, list = self._getlongresp(file)
527 return resp, [line.decode(self.encoding, self.errors)
528 for line in list]
529
530 def _getoverviewfmt(self):
531 """Internal: get the overview format. Queries the server if not
532 already done, else returns the cached value."""
533 try:
534 return self._cachedoverviewfmt
535 except AttributeError:
536 pass
537 try:
538 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
539 except NNTPPermanentError:
540 # Not supported by server?
541 fmt = _DEFAULT_OVERVIEW_FMT[:]
542 else:
543 fmt = _parse_overview_fmt(lines)
544 self._cachedoverviewfmt = fmt
545 return fmt
546
547 def _grouplist(self, lines):
548 # Parse lines into "group last first flag"
549 return [GroupInfo(*line.split()) for line in lines]
550
551 def capabilities(self):
552 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000553 Return:
554 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000555 - caps: a dictionary mapping capability names to lists of tokens
556 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
557 """
558 caps = {}
559 resp, lines = self._longcmdstring("CAPABILITIES")
560 for line in lines:
561 name, *tokens = line.split()
562 caps[name] = tokens
563 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000564
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000565 def newgroups(self, date, *, file=None):
566 """Process a NEWGROUPS command. Arguments:
567 - date: a date or datetime object
568 Return:
569 - resp: server response if successful
570 - list: list of newsgroup names
571 """
572 if not isinstance(date, (datetime.date, datetime.date)):
573 raise TypeError(
574 "the date parameter must be a date or datetime object, "
575 "not '{:40}'".format(date.__class__.__name__))
576 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
577 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
578 resp, lines = self._longcmdstring(cmd, file)
579 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000580
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000581 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000582 """Process a NEWNEWS command. Arguments:
583 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000584 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000585 Return:
586 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000587 - list: list of message ids
588 """
589 if not isinstance(date, (datetime.date, datetime.date)):
590 raise TypeError(
591 "the date parameter must be a date or datetime object, "
592 "not '{:40}'".format(date.__class__.__name__))
593 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
594 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
595 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000596
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000597 def list(self, group_pattern=None, *, file=None):
598 """Process a LIST or LIST ACTIVE command. Arguments:
599 - group_pattern: a pattern indicating which groups to query
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000600 - file: Filename string or file object to store the result in
601 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000602 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000603 - list: list of (group, last, first, flag) (strings)
604 """
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000605 if group_pattern is not None:
606 command = 'LIST ACTIVE ' + group_pattern
607 else:
608 command = 'LIST'
609 resp, lines = self._longcmdstring(command, file)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000610 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000611
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000612 def _getdescriptions(self, group_pattern, return_all):
613 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
614 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
615 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
616 if not resp.startswith('215'):
617 # Now the deprecated XGTITLE. This either raises an error
618 # or succeeds with the same output structure as LIST
619 # NEWSGROUPS.
620 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
621 groups = {}
622 for raw_line in lines:
623 match = line_pat.search(raw_line.strip())
624 if match:
625 name, desc = match.group(1, 2)
626 if not return_all:
627 return desc
628 groups[name] = desc
629 if return_all:
630 return resp, groups
631 else:
632 # Nothing found
633 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000634
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000635 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000636 """Get a description for a single group. If more than one
637 group matches ('group' is a pattern), return the first. If no
638 group matches, return an empty string.
639
640 This elides the response code from the server, since it can
641 only be '215' or '285' (for xgtitle) anyway. If the response
642 code is needed, use the 'descriptions' method.
643
644 NOTE: This neither checks for a wildcard in 'group' nor does
645 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000646 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000647
648 def descriptions(self, group_pattern):
649 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000650 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000651
Tim Peters2344fae2001-01-15 00:50:52 +0000652 def group(self, name):
653 """Process a GROUP command. Argument:
654 - group: the group name
655 Returns:
656 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000657 - count: number of articles
658 - first: first article number
659 - last: last article number
660 - name: the group name
661 """
662 resp = self._shortcmd('GROUP ' + name)
663 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000664 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000665 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000666 count = first = last = 0
667 n = len(words)
668 if n > 1:
669 count = words[1]
670 if n > 2:
671 first = words[2]
672 if n > 3:
673 last = words[3]
674 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000675 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000676 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000677
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000678 def help(self, *, file=None):
679 """Process a HELP command. Argument:
680 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000681 Returns:
682 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000683 - list: list of strings returned by the server in response to the
684 HELP command
685 """
686 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000687
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000688 def _statparse(self, resp):
689 """Internal: parse the response line of a STAT, NEXT, LAST,
690 ARTICLE, HEAD or BODY command."""
691 if not resp.startswith('22'):
692 raise NNTPReplyError(resp)
693 words = resp.split()
694 art_num = int(words[1])
695 message_id = words[2]
696 return resp, art_num, message_id
697
698 def _statcmd(self, line):
699 """Internal: process a STAT, NEXT or LAST command."""
700 resp = self._shortcmd(line)
701 return self._statparse(resp)
702
703 def stat(self, message_spec=None):
704 """Process a STAT command. Argument:
705 - message_spec: article number or message id (if not specified,
706 the current article is selected)
707 Returns:
708 - resp: server response if successful
709 - art_num: the article number
710 - message_id: the message id
711 """
712 if message_spec:
713 return self._statcmd('STAT {0}'.format(message_spec))
714 else:
715 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000716
Tim Peters2344fae2001-01-15 00:50:52 +0000717 def next(self):
718 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000719 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000720
Tim Peters2344fae2001-01-15 00:50:52 +0000721 def last(self):
722 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000723 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000724
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000725 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000726 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000727 resp, lines = self._longcmd(line, file)
728 resp, art_num, message_id = self._statparse(resp)
729 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000730
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000731 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000732 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000733 - message_spec: article number or message id
734 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000735 Returns:
736 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000737 - ArticleInfo: (article number, message id, list of header lines)
738 """
739 if message_spec is not None:
740 cmd = 'HEAD {0}'.format(message_spec)
741 else:
742 cmd = 'HEAD'
743 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000744
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000745 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000746 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000747 - message_spec: article number or message id
748 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000749 Returns:
750 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000751 - ArticleInfo: (article number, message id, list of body lines)
752 """
753 if message_spec is not None:
754 cmd = 'BODY {0}'.format(message_spec)
755 else:
756 cmd = 'BODY'
757 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000758
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000759 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000760 """Process an ARTICLE 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 article 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 article lines)
766 """
767 if message_spec is not None:
768 cmd = 'ARTICLE {0}'.format(message_spec)
769 else:
770 cmd = 'ARTICLE'
771 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000772
Tim Peters2344fae2001-01-15 00:50:52 +0000773 def slave(self):
774 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000775 - resp: server response if successful
776 """
777 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000778
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000779 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000780 """Process an XHDR command (optional server extension). Arguments:
781 - hdr: the header type (e.g. 'subject')
782 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000783 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000784 Returns:
785 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000786 - list: list of (nr, value) strings
787 """
788 pat = re.compile('^([0-9]+) ?(.*)\n?')
789 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
790 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000791 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000792 return m.group(1, 2) if m else line
793 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000794
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000795 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000796 """Process an XOVER command (optional server extension) Arguments:
797 - start: start of range
798 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000799 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000800 Returns:
801 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000802 - list: list of dicts containing the response fields
803 """
804 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
805 file)
806 fmt = self._getoverviewfmt()
807 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000808
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000809 def over(self, message_spec, *, file=None):
810 """Process an OVER command. If the command isn't supported, fall
811 back to XOVER. Arguments:
812 - message_spec:
813 - either a message id, indicating the article to fetch
814 information about
815 - or a (start, end) tuple, indicating a range of article numbers;
816 if end is None, information up to the newest message will be
817 retrieved
818 - or None, indicating the current article number must be used
819 - file: Filename string or file object to store the result in
820 Returns:
821 - resp: server response if successful
822 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000823
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000824 NOTE: the "message id" form isn't supported by XOVER
825 """
826 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
827 if isinstance(message_spec, (tuple, list)):
828 start, end = message_spec
829 cmd += ' {0}-{1}'.format(start, end or '')
830 elif message_spec is not None:
831 cmd = cmd + ' ' + message_spec
832 resp, lines = self._longcmdstring(cmd, file)
833 fmt = self._getoverviewfmt()
834 return resp, _parse_overview(lines, fmt)
835
836 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000837 """Process an XGTITLE command (optional server extension) Arguments:
838 - group: group name wildcard (i.e. news.*)
839 Returns:
840 - resp: server response if successful
841 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000842 warnings.warn("The XGTITLE extension is not actively used, "
843 "use descriptions() instead",
Florent Xicluna67317752011-12-10 11:07:42 +0100844 DeprecationWarning, 2)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000845 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
846 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000847 lines = []
848 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000849 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000850 if match:
851 lines.append(match.group(1, 2))
852 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000853
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000854 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000855 """Process an XPATH command (optional server extension) Arguments:
856 - id: Message id of article
857 Returns:
858 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000859 path: directory path to article
860 """
861 warnings.warn("The XPATH extension is not actively used",
Florent Xicluna67317752011-12-10 11:07:42 +0100862 DeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000863
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000864 resp = self._shortcmd('XPATH {0}'.format(id))
865 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000866 raise NNTPReplyError(resp)
867 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000868 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000869 except ValueError:
870 raise NNTPReplyError(resp)
871 else:
872 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000873
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000874 def date(self):
875 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000876 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000877 - resp: server response if successful
878 - date: datetime object
879 """
880 resp = self._shortcmd("DATE")
881 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000882 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000883 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000884 if len(elem) != 2:
885 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000886 date = elem[1]
887 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000888 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000889 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000890
Christian Heimes933238a2008-11-05 19:44:21 +0000891 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000892 resp = self._shortcmd(command)
893 # Raises a specific exception if posting is not allowed
894 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000895 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000896 if isinstance(f, (bytes, bytearray)):
897 f = f.splitlines()
898 # We don't use _putline() because:
899 # - we don't want additional CRLF if the file or iterable is already
900 # in the right format
901 # - we don't want a spurious flush() after each line is written
902 for line in f:
903 if not line.endswith(_CRLF):
904 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000905 if line.startswith(b'.'):
906 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000907 self.file.write(line)
908 self.file.write(b".\r\n")
909 self.file.flush()
910 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000911
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000912 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000913 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000914 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000915 Returns:
916 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000917 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000918
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000919 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000920 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000921 - message_id: message-id of the article
922 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000923 Returns:
924 - resp: server response if successful
925 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000926 return self._post('IHAVE {0}'.format(message_id), data)
927
928 def _close(self):
929 self.file.close()
930 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000931
Tim Peters2344fae2001-01-15 00:50:52 +0000932 def quit(self):
933 """Process a QUIT command and close the socket. Returns:
934 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000935 try:
936 resp = self._shortcmd('QUIT')
937 finally:
938 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000939 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000940
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000941 def login(self, user=None, password=None, usenetrc=True):
942 if self.authenticated:
943 raise ValueError("Already logged in.")
944 if not user and not usenetrc:
945 raise ValueError(
946 "At least one of `user` and `usenetrc` must be specified")
947 # If no login/password was specified but netrc was requested,
948 # try to get them from ~/.netrc
949 # Presume that if .netrc has an entry, NNRP authentication is required.
950 try:
951 if usenetrc and not user:
952 import netrc
953 credentials = netrc.netrc()
954 auth = credentials.authenticators(self.host)
955 if auth:
956 user = auth[0]
957 password = auth[2]
Andrew Svetlovf7a17b42012-12-25 16:47:37 +0200958 except OSError:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000959 pass
960 # Perform NNTP authentication if needed.
961 if not user:
962 return
963 resp = self._shortcmd('authinfo user ' + user)
964 if resp.startswith('381'):
965 if not password:
966 raise NNTPReplyError(resp)
967 else:
968 resp = self._shortcmd('authinfo pass ' + password)
969 if not resp.startswith('281'):
970 raise NNTPPermanentError(resp)
Antoine Pitrou54411c12012-02-12 19:14:17 +0100971 # Capabilities might have changed after login
972 self._caps = None
973 self.getcapabilities()
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000974 # Attempt to send mode reader if it was requested after login.
Antoine Pitrou71135622012-02-14 23:29:34 +0100975 # Only do so if we're not in reader mode already.
976 if self.readermode_afterauth and 'READER' not in self._caps:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000977 self._setreadermode()
Antoine Pitrou71135622012-02-14 23:29:34 +0100978 # Capabilities might have changed after MODE READER
979 self._caps = None
980 self.getcapabilities()
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000981
982 def _setreadermode(self):
983 try:
984 self.welcome = self._shortcmd('mode reader')
985 except NNTPPermanentError:
986 # Error 5xx, probably 'not implemented'
987 pass
988 except NNTPTemporaryError as e:
989 if e.response.startswith('480'):
990 # Need authorization before 'mode reader'
991 self.readermode_afterauth = True
992 else:
993 raise
994
995 if _have_ssl:
996 def starttls(self, context=None):
997 """Process a STARTTLS command. Arguments:
998 - context: SSL context to use for the encrypted connection
999 """
1000 # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if
1001 # a TLS session already exists.
1002 if self.tls_on:
1003 raise ValueError("TLS is already enabled.")
1004 if self.authenticated:
1005 raise ValueError("TLS cannot be started after authentication.")
1006 resp = self._shortcmd('STARTTLS')
1007 if resp.startswith('382'):
1008 self.file.close()
Christian Heimes216d4632013-12-02 20:20:11 +01001009 self.sock = _encrypt_on(self.sock, context, self.host)
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001010 self.file = self.sock.makefile("rwb")
1011 self.tls_on = True
1012 # Capabilities may change after TLS starts up, so ask for them
1013 # again.
1014 self._caps = None
1015 self.getcapabilities()
1016 else:
1017 raise NNTPError("TLS failed to start.")
1018
Guido van Rossume2ed9df1997-08-26 23:26:18 +00001019
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001020class NNTP(_NNTPBase):
1021
1022 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001023 readermode=None, usenetrc=False,
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001024 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1025 """Initialize an instance. Arguments:
1026 - host: hostname to connect to
1027 - port: port to connect to (default the standard NNTP port)
1028 - user: username to authenticate with
1029 - password: password to use with username
1030 - readermode: if true, send 'mode reader' command after
1031 connecting.
1032 - usenetrc: allow loading username and password from ~/.netrc file
1033 if not specified explicitly
1034 - timeout: timeout (in seconds) used for socket connections
1035
1036 readermode is sometimes necessary if you are connecting to an
1037 NNTP server on the local machine and intend to call
Ezio Melotti4969f702011-03-15 05:59:46 +02001038 reader-specific commands, such as `group'. If you get
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001039 unexpected NNTPPermanentErrors, you might need to set
1040 readermode.
1041 """
1042 self.host = host
1043 self.port = port
1044 self.sock = socket.create_connection((host, port), timeout)
1045 file = self.sock.makefile("rwb")
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001046 _NNTPBase.__init__(self, file, host,
1047 readermode, timeout)
1048 if user or usenetrc:
1049 self.login(user, password, usenetrc)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001050
1051 def _close(self):
1052 try:
1053 _NNTPBase._close(self)
1054 finally:
1055 self.sock.close()
1056
1057
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001058if _have_ssl:
1059 class NNTP_SSL(_NNTPBase):
1060
1061 def __init__(self, host, port=NNTP_SSL_PORT,
1062 user=None, password=None, ssl_context=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001063 readermode=None, usenetrc=False,
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001064 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1065 """This works identically to NNTP.__init__, except for the change
1066 in default port and the `ssl_context` argument for SSL connections.
1067 """
1068 self.sock = socket.create_connection((host, port), timeout)
Christian Heimes216d4632013-12-02 20:20:11 +01001069 self.sock = _encrypt_on(self.sock, ssl_context, host)
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001070 file = self.sock.makefile("rwb")
1071 _NNTPBase.__init__(self, file, host,
1072 readermode=readermode, timeout=timeout)
1073 if user or usenetrc:
1074 self.login(user, password, usenetrc)
1075
1076 def _close(self):
1077 try:
1078 _NNTPBase._close(self)
1079 finally:
1080 self.sock.close()
1081
1082 __all__.append("NNTP_SSL")
1083
1084
Neal Norwitzef679562002-11-14 02:19:44 +00001085# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +00001086if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001087 import argparse
1088 from email.utils import parsedate
1089
1090 parser = argparse.ArgumentParser(description="""\
1091 nntplib built-in demo - display the latest articles in a newsgroup""")
1092 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
1093 help='group to fetch messages from (default: %(default)s)')
1094 parser.add_argument('-s', '--server', default='news.gmane.org',
1095 help='NNTP server hostname (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001096 parser.add_argument('-p', '--port', default=-1, type=int,
1097 help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT))
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001098 parser.add_argument('-n', '--nb-articles', default=10, type=int,
1099 help='number of articles to fetch (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001100 parser.add_argument('-S', '--ssl', action='store_true', default=False,
1101 help='use NNTP over SSL')
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001102 args = parser.parse_args()
1103
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001104 port = args.port
1105 if not args.ssl:
1106 if port == -1:
1107 port = NNTP_PORT
1108 s = NNTP(host=args.server, port=port)
1109 else:
1110 if port == -1:
1111 port = NNTP_SSL_PORT
1112 s = NNTP_SSL(host=args.server, port=port)
1113
1114 caps = s.getcapabilities()
1115 if 'STARTTLS' in caps:
1116 s.starttls()
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001117 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001118 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001119
1120 def cut(s, lim):
1121 if len(s) > lim:
1122 s = s[:lim - 4] + "..."
1123 return s
1124
1125 first = str(int(last) - args.nb_articles + 1)
1126 resp, overviews = s.xover(first, last)
1127 for artnum, over in overviews:
1128 author = decode_header(over['from']).split('<', 1)[0]
1129 subject = decode_header(over['subject'])
1130 lines = int(over[':lines'])
1131 print("{:7} {:20} {:42} ({})".format(
1132 artnum, cut(author, 20), cut(subject, 42), lines)
1133 )
1134
1135 s.quit()