blob: 9766be3bc5ba5b54c756483a7e875a41bf12c79a [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
282 def _encrypt_on(sock, context):
283 """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:
291 context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
292 # SSLv2 considered harmful.
293 context.options |= ssl.OP_NO_SSLv2
294 return context.wrap_socket(sock)
295
296
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000297# The classes themselves
298class _NNTPBase:
299 # UTF-8 is the character set for all NNTP commands and responses: they
300 # are automatically encoded (when sending) and decoded (and receiving)
301 # by this class.
302 # However, some multi-line data blocks can contain arbitrary bytes (for
303 # example, latin-1 or utf-16 data in the body of a message). Commands
304 # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
305 # data will therefore only accept and produce bytes objects.
306 # Furthermore, since there could be non-compliant servers out there,
307 # we use 'surrogateescape' as the error handler for fault tolerance
308 # and easy round-tripping. This could be useful for some applications
309 # (e.g. NNTP gateways).
310
311 encoding = 'utf-8'
312 errors = 'surrogateescape'
313
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000314 def __init__(self, file, host,
315 readermode=None, timeout=_GLOBAL_DEFAULT_TIMEOUT):
Tim Peters2344fae2001-01-15 00:50:52 +0000316 """Initialize an instance. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000317 - file: file-like object (open for read/write in binary mode)
Antoine Pitrou859c4ef2010-11-09 18:58:42 +0000318 - host: hostname of the server
Tim Peters2344fae2001-01-15 00:50:52 +0000319 - readermode: if true, send 'mode reader' command after
320 connecting.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000321 - timeout: timeout (in seconds) used for socket connections
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000322
Tim Peters2344fae2001-01-15 00:50:52 +0000323 readermode is sometimes necessary if you are connecting to an
324 NNTP server on the local machine and intend to call
Ezio Melotti42da6632011-03-15 05:18:48 +0200325 reader-specific commands, such as `group'. If you get
Tim Peters2344fae2001-01-15 00:50:52 +0000326 unexpected NNTPPermanentErrors, you might need to set
327 readermode.
328 """
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000329 self.host = host
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000330 self.file = file
Tim Peters2344fae2001-01-15 00:50:52 +0000331 self.debugging = 0
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000332 self.welcome = self._getresp()
Tim Petersdfb673b2001-01-16 07:12:46 +0000333
Antoine Pitrou71135622012-02-14 23:29:34 +0100334 # Inquire about capabilities (RFC 3977).
335 self._caps = None
336 self.getcapabilities()
337
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000338 # 'MODE READER' is sometimes necessary to enable 'reader' mode.
339 # However, the order in which 'MODE READER' and 'AUTHINFO' need to
340 # arrive differs between some NNTP servers. If _setreadermode() fails
341 # with an authorization failed error, it will set this to True;
342 # the login() routine will interpret that as a request to try again
343 # after performing its normal function.
Antoine Pitrou71135622012-02-14 23:29:34 +0100344 # Enable only if we're not already in READER mode anyway.
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000345 self.readermode_afterauth = False
Antoine Pitrou71135622012-02-14 23:29:34 +0100346 if readermode and 'READER' not in self._caps:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000347 self._setreadermode()
Antoine Pitrou71135622012-02-14 23:29:34 +0100348 if not self.readermode_afterauth:
349 # Capabilities might have changed after MODE READER
350 self._caps = None
351 self.getcapabilities()
Tim Petersdfb673b2001-01-16 07:12:46 +0000352
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000353 # RFC 4642 2.2.2: Both the client and the server MUST know if there is
354 # a TLS session active. A client MUST NOT attempt to start a TLS
355 # session if a TLS session is already active.
356 self.tls_on = False
357
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000358 # Log in and encryption setup order is left to subclasses.
359 self.authenticated = False
Guido van Rossumc629d341992-11-05 10:43:02 +0000360
Giampaolo Rodolà424298a2011-03-03 18:34:06 +0000361 def __enter__(self):
362 return self
363
364 def __exit__(self, *args):
365 is_connected = lambda: hasattr(self, "file")
366 if is_connected():
367 try:
368 self.quit()
Andrew Svetlov0832af62012-12-18 23:10:48 +0200369 except (OSError, EOFError):
Giampaolo Rodolà424298a2011-03-03 18:34:06 +0000370 pass
371 finally:
372 if is_connected():
373 self._close()
374
Tim Peters2344fae2001-01-15 00:50:52 +0000375 def getwelcome(self):
376 """Get the welcome message from the server
377 (this is read and squirreled away by __init__()).
378 If the response code is 200, posting is allowed;
379 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000380
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000381 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000382 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000383
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000384 def getcapabilities(self):
385 """Get the server capabilities, as read by __init__().
386 If the CAPABILITIES command is not supported, an empty dict is
387 returned."""
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000388 if self._caps is None:
389 self.nntp_version = 1
390 self.nntp_implementation = None
391 try:
392 resp, caps = self.capabilities()
Antoine Pitrou54411c12012-02-12 19:14:17 +0100393 except (NNTPPermanentError, NNTPTemporaryError):
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000394 # Server doesn't support capabilities
395 self._caps = {}
396 else:
397 self._caps = caps
398 if 'VERSION' in caps:
399 # The server can advertise several supported versions,
400 # choose the highest.
401 self.nntp_version = max(map(int, caps['VERSION']))
402 if 'IMPLEMENTATION' in caps:
403 self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000404 return self._caps
405
Tim Peters2344fae2001-01-15 00:50:52 +0000406 def set_debuglevel(self, level):
407 """Set the debugging level. Argument 'level' means:
408 0: no debugging output (default)
409 1: print commands and responses but not body text etc.
410 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000411
Tim Peters2344fae2001-01-15 00:50:52 +0000412 self.debugging = level
413 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000414
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000415 def _putline(self, line):
416 """Internal: send one line to the server, appending CRLF.
417 The `line` must be a bytes-like object."""
418 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000419 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000420 self.file.write(line)
421 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000422
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000423 def _putcmd(self, line):
424 """Internal: send one command to the server (through _putline()).
425 The `line` must be an unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000426 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000427 line = line.encode(self.encoding, self.errors)
428 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000429
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000430 def _getline(self, strip_crlf=True):
431 """Internal: return one line from the server, stripping _CRLF.
432 Raise EOFError if the connection is closed.
433 Returns a bytes object."""
Georg Brandl28e78412013-10-27 07:29:47 +0100434 line = self.file.readline(_MAXLINE +1)
435 if len(line) > _MAXLINE:
436 raise NNTPDataError('line too long')
Tim Peters2344fae2001-01-15 00:50:52 +0000437 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000438 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000439 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000440 if strip_crlf:
441 if line[-2:] == _CRLF:
442 line = line[:-2]
443 elif line[-1:] in _CRLF:
444 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000445 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000446
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000447 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000448 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000449 Raise various errors if the response indicates an error.
450 Returns an unicode string."""
451 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000452 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000453 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000454 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000455 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000456 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000457 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000458 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000459 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000460 raise NNTPProtocolError(resp)
461 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000462
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000463 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000464 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000465 Raise various errors if the response indicates an error.
466
467 Returns a (response, lines) tuple where `response` is an unicode
468 string and `lines` is a list of bytes objects.
469 If `file` is a file-like object, it must be open in binary mode.
470 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000471
472 openedFile = None
473 try:
474 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000475 if isinstance(file, (str, bytes)):
476 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000477
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000478 resp = self._getresp()
479 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000480 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000481
482 lines = []
483 if file is not None:
484 # XXX lines = None instead?
485 terminators = (b'.' + _CRLF, b'.\n')
486 while 1:
487 line = self._getline(False)
488 if line in terminators:
489 break
490 if line.startswith(b'..'):
491 line = line[1:]
492 file.write(line)
493 else:
494 terminator = b'.'
495 while 1:
496 line = self._getline()
497 if line == terminator:
498 break
499 if line.startswith(b'..'):
500 line = line[1:]
501 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000502 finally:
503 # If this method created the file, then it must close it
504 if openedFile:
505 openedFile.close()
506
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000507 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000508
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000509 def _shortcmd(self, line):
510 """Internal: send a command and get the response.
511 Same return value as _getresp()."""
512 self._putcmd(line)
513 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000514
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000515 def _longcmd(self, line, file=None):
516 """Internal: send a command and get the response plus following text.
517 Same return value as _getlongresp()."""
518 self._putcmd(line)
519 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000520
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000521 def _longcmdstring(self, line, file=None):
522 """Internal: send a command and get the response plus following text.
523 Same as _longcmd() and _getlongresp(), except that the returned `lines`
524 are unicode strings rather than bytes objects.
525 """
526 self._putcmd(line)
527 resp, list = self._getlongresp(file)
528 return resp, [line.decode(self.encoding, self.errors)
529 for line in list]
530
531 def _getoverviewfmt(self):
532 """Internal: get the overview format. Queries the server if not
533 already done, else returns the cached value."""
534 try:
535 return self._cachedoverviewfmt
536 except AttributeError:
537 pass
538 try:
539 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
540 except NNTPPermanentError:
541 # Not supported by server?
542 fmt = _DEFAULT_OVERVIEW_FMT[:]
543 else:
544 fmt = _parse_overview_fmt(lines)
545 self._cachedoverviewfmt = fmt
546 return fmt
547
548 def _grouplist(self, lines):
549 # Parse lines into "group last first flag"
550 return [GroupInfo(*line.split()) for line in lines]
551
552 def capabilities(self):
553 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000554 Return:
555 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000556 - caps: a dictionary mapping capability names to lists of tokens
557 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
558 """
559 caps = {}
560 resp, lines = self._longcmdstring("CAPABILITIES")
561 for line in lines:
562 name, *tokens = line.split()
563 caps[name] = tokens
564 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000565
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000566 def newgroups(self, date, *, file=None):
567 """Process a NEWGROUPS command. Arguments:
568 - date: a date or datetime object
569 Return:
570 - resp: server response if successful
571 - list: list of newsgroup names
572 """
573 if not isinstance(date, (datetime.date, datetime.date)):
574 raise TypeError(
575 "the date parameter must be a date or datetime object, "
576 "not '{:40}'".format(date.__class__.__name__))
577 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
578 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
579 resp, lines = self._longcmdstring(cmd, file)
580 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000581
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000582 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000583 """Process a NEWNEWS command. Arguments:
584 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000585 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000586 Return:
587 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000588 - list: list of message ids
589 """
590 if not isinstance(date, (datetime.date, datetime.date)):
591 raise TypeError(
592 "the date parameter must be a date or datetime object, "
593 "not '{:40}'".format(date.__class__.__name__))
594 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
595 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
596 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000597
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000598 def list(self, group_pattern=None, *, file=None):
599 """Process a LIST or LIST ACTIVE command. Arguments:
600 - group_pattern: a pattern indicating which groups to query
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000601 - file: Filename string or file object to store the result in
602 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000603 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000604 - list: list of (group, last, first, flag) (strings)
605 """
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000606 if group_pattern is not None:
607 command = 'LIST ACTIVE ' + group_pattern
608 else:
609 command = 'LIST'
610 resp, lines = self._longcmdstring(command, file)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000611 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000612
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000613 def _getdescriptions(self, group_pattern, return_all):
614 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
615 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
616 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
617 if not resp.startswith('215'):
618 # Now the deprecated XGTITLE. This either raises an error
619 # or succeeds with the same output structure as LIST
620 # NEWSGROUPS.
621 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
622 groups = {}
623 for raw_line in lines:
624 match = line_pat.search(raw_line.strip())
625 if match:
626 name, desc = match.group(1, 2)
627 if not return_all:
628 return desc
629 groups[name] = desc
630 if return_all:
631 return resp, groups
632 else:
633 # Nothing found
634 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000635
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000636 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000637 """Get a description for a single group. If more than one
638 group matches ('group' is a pattern), return the first. If no
639 group matches, return an empty string.
640
641 This elides the response code from the server, since it can
642 only be '215' or '285' (for xgtitle) anyway. If the response
643 code is needed, use the 'descriptions' method.
644
645 NOTE: This neither checks for a wildcard in 'group' nor does
646 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000647 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000648
649 def descriptions(self, group_pattern):
650 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000651 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000652
Tim Peters2344fae2001-01-15 00:50:52 +0000653 def group(self, name):
654 """Process a GROUP command. Argument:
655 - group: the group name
656 Returns:
657 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000658 - count: number of articles
659 - first: first article number
660 - last: last article number
661 - name: the group name
662 """
663 resp = self._shortcmd('GROUP ' + name)
664 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000665 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000666 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000667 count = first = last = 0
668 n = len(words)
669 if n > 1:
670 count = words[1]
671 if n > 2:
672 first = words[2]
673 if n > 3:
674 last = words[3]
675 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000676 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000677 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000678
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000679 def help(self, *, file=None):
680 """Process a HELP command. Argument:
681 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000682 Returns:
683 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000684 - list: list of strings returned by the server in response to the
685 HELP command
686 """
687 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000688
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000689 def _statparse(self, resp):
690 """Internal: parse the response line of a STAT, NEXT, LAST,
691 ARTICLE, HEAD or BODY command."""
692 if not resp.startswith('22'):
693 raise NNTPReplyError(resp)
694 words = resp.split()
695 art_num = int(words[1])
696 message_id = words[2]
697 return resp, art_num, message_id
698
699 def _statcmd(self, line):
700 """Internal: process a STAT, NEXT or LAST command."""
701 resp = self._shortcmd(line)
702 return self._statparse(resp)
703
704 def stat(self, message_spec=None):
705 """Process a STAT command. Argument:
706 - message_spec: article number or message id (if not specified,
707 the current article is selected)
708 Returns:
709 - resp: server response if successful
710 - art_num: the article number
711 - message_id: the message id
712 """
713 if message_spec:
714 return self._statcmd('STAT {0}'.format(message_spec))
715 else:
716 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000717
Tim Peters2344fae2001-01-15 00:50:52 +0000718 def next(self):
719 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000720 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000721
Tim Peters2344fae2001-01-15 00:50:52 +0000722 def last(self):
723 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000724 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000725
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000726 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000727 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000728 resp, lines = self._longcmd(line, file)
729 resp, art_num, message_id = self._statparse(resp)
730 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000731
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000732 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000733 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000734 - message_spec: article number or message id
735 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000736 Returns:
737 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000738 - ArticleInfo: (article number, message id, list of header lines)
739 """
740 if message_spec is not None:
741 cmd = 'HEAD {0}'.format(message_spec)
742 else:
743 cmd = 'HEAD'
744 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000745
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000746 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000747 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000748 - message_spec: article number or message id
749 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000750 Returns:
751 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000752 - ArticleInfo: (article number, message id, list of body lines)
753 """
754 if message_spec is not None:
755 cmd = 'BODY {0}'.format(message_spec)
756 else:
757 cmd = 'BODY'
758 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000759
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000760 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000761 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000762 - message_spec: article number or message id
763 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000764 Returns:
765 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000766 - ArticleInfo: (article number, message id, list of article lines)
767 """
768 if message_spec is not None:
769 cmd = 'ARTICLE {0}'.format(message_spec)
770 else:
771 cmd = 'ARTICLE'
772 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000773
Tim Peters2344fae2001-01-15 00:50:52 +0000774 def slave(self):
775 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000776 - resp: server response if successful
777 """
778 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000779
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000780 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000781 """Process an XHDR command (optional server extension). Arguments:
782 - hdr: the header type (e.g. 'subject')
783 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000784 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000785 Returns:
786 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000787 - list: list of (nr, value) strings
788 """
789 pat = re.compile('^([0-9]+) ?(.*)\n?')
790 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
791 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000792 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000793 return m.group(1, 2) if m else line
794 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000795
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000796 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000797 """Process an XOVER command (optional server extension) Arguments:
798 - start: start of range
799 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000800 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000801 Returns:
802 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000803 - list: list of dicts containing the response fields
804 """
805 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
806 file)
807 fmt = self._getoverviewfmt()
808 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000809
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000810 def over(self, message_spec, *, file=None):
811 """Process an OVER command. If the command isn't supported, fall
812 back to XOVER. Arguments:
813 - message_spec:
814 - either a message id, indicating the article to fetch
815 information about
816 - or a (start, end) tuple, indicating a range of article numbers;
817 if end is None, information up to the newest message will be
818 retrieved
819 - or None, indicating the current article number must be used
820 - file: Filename string or file object to store the result in
821 Returns:
822 - resp: server response if successful
823 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000824
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000825 NOTE: the "message id" form isn't supported by XOVER
826 """
827 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
828 if isinstance(message_spec, (tuple, list)):
829 start, end = message_spec
830 cmd += ' {0}-{1}'.format(start, end or '')
831 elif message_spec is not None:
832 cmd = cmd + ' ' + message_spec
833 resp, lines = self._longcmdstring(cmd, file)
834 fmt = self._getoverviewfmt()
835 return resp, _parse_overview(lines, fmt)
836
837 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000838 """Process an XGTITLE command (optional server extension) Arguments:
839 - group: group name wildcard (i.e. news.*)
840 Returns:
841 - resp: server response if successful
842 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000843 warnings.warn("The XGTITLE extension is not actively used, "
844 "use descriptions() instead",
Florent Xicluna67317752011-12-10 11:07:42 +0100845 DeprecationWarning, 2)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000846 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
847 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000848 lines = []
849 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000850 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000851 if match:
852 lines.append(match.group(1, 2))
853 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000854
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000855 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000856 """Process an XPATH command (optional server extension) Arguments:
857 - id: Message id of article
858 Returns:
859 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000860 path: directory path to article
861 """
862 warnings.warn("The XPATH extension is not actively used",
Florent Xicluna67317752011-12-10 11:07:42 +0100863 DeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000864
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000865 resp = self._shortcmd('XPATH {0}'.format(id))
866 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000867 raise NNTPReplyError(resp)
868 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000869 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000870 except ValueError:
871 raise NNTPReplyError(resp)
872 else:
873 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000874
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000875 def date(self):
876 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000877 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000878 - resp: server response if successful
879 - date: datetime object
880 """
881 resp = self._shortcmd("DATE")
882 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000883 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000884 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000885 if len(elem) != 2:
886 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000887 date = elem[1]
888 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000889 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000890 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000891
Christian Heimes933238a2008-11-05 19:44:21 +0000892 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000893 resp = self._shortcmd(command)
894 # Raises a specific exception if posting is not allowed
895 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000896 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000897 if isinstance(f, (bytes, bytearray)):
898 f = f.splitlines()
899 # We don't use _putline() because:
900 # - we don't want additional CRLF if the file or iterable is already
901 # in the right format
902 # - we don't want a spurious flush() after each line is written
903 for line in f:
904 if not line.endswith(_CRLF):
905 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000906 if line.startswith(b'.'):
907 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000908 self.file.write(line)
909 self.file.write(b".\r\n")
910 self.file.flush()
911 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000912
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000913 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000914 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000915 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000916 Returns:
917 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000918 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000919
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000920 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000921 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000922 - message_id: message-id of the article
923 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000924 Returns:
925 - resp: server response if successful
926 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000927 return self._post('IHAVE {0}'.format(message_id), data)
928
929 def _close(self):
930 self.file.close()
931 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000932
Tim Peters2344fae2001-01-15 00:50:52 +0000933 def quit(self):
934 """Process a QUIT command and close the socket. Returns:
935 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000936 try:
937 resp = self._shortcmd('QUIT')
938 finally:
939 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000940 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000941
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000942 def login(self, user=None, password=None, usenetrc=True):
943 if self.authenticated:
944 raise ValueError("Already logged in.")
945 if not user and not usenetrc:
946 raise ValueError(
947 "At least one of `user` and `usenetrc` must be specified")
948 # If no login/password was specified but netrc was requested,
949 # try to get them from ~/.netrc
950 # Presume that if .netrc has an entry, NNRP authentication is required.
951 try:
952 if usenetrc and not user:
953 import netrc
954 credentials = netrc.netrc()
955 auth = credentials.authenticators(self.host)
956 if auth:
957 user = auth[0]
958 password = auth[2]
Andrew Svetlovf7a17b42012-12-25 16:47:37 +0200959 except OSError:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000960 pass
961 # Perform NNTP authentication if needed.
962 if not user:
963 return
964 resp = self._shortcmd('authinfo user ' + user)
965 if resp.startswith('381'):
966 if not password:
967 raise NNTPReplyError(resp)
968 else:
969 resp = self._shortcmd('authinfo pass ' + password)
970 if not resp.startswith('281'):
971 raise NNTPPermanentError(resp)
Antoine Pitrou54411c12012-02-12 19:14:17 +0100972 # Capabilities might have changed after login
973 self._caps = None
974 self.getcapabilities()
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000975 # Attempt to send mode reader if it was requested after login.
Antoine Pitrou71135622012-02-14 23:29:34 +0100976 # Only do so if we're not in reader mode already.
977 if self.readermode_afterauth and 'READER' not in self._caps:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000978 self._setreadermode()
Antoine Pitrou71135622012-02-14 23:29:34 +0100979 # Capabilities might have changed after MODE READER
980 self._caps = None
981 self.getcapabilities()
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000982
983 def _setreadermode(self):
984 try:
985 self.welcome = self._shortcmd('mode reader')
986 except NNTPPermanentError:
987 # Error 5xx, probably 'not implemented'
988 pass
989 except NNTPTemporaryError as e:
990 if e.response.startswith('480'):
991 # Need authorization before 'mode reader'
992 self.readermode_afterauth = True
993 else:
994 raise
995
996 if _have_ssl:
997 def starttls(self, context=None):
998 """Process a STARTTLS command. Arguments:
999 - context: SSL context to use for the encrypted connection
1000 """
1001 # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if
1002 # a TLS session already exists.
1003 if self.tls_on:
1004 raise ValueError("TLS is already enabled.")
1005 if self.authenticated:
1006 raise ValueError("TLS cannot be started after authentication.")
1007 resp = self._shortcmd('STARTTLS')
1008 if resp.startswith('382'):
1009 self.file.close()
1010 self.sock = _encrypt_on(self.sock, context)
1011 self.file = self.sock.makefile("rwb")
1012 self.tls_on = True
1013 # Capabilities may change after TLS starts up, so ask for them
1014 # again.
1015 self._caps = None
1016 self.getcapabilities()
1017 else:
1018 raise NNTPError("TLS failed to start.")
1019
Guido van Rossume2ed9df1997-08-26 23:26:18 +00001020
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001021class NNTP(_NNTPBase):
1022
1023 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001024 readermode=None, usenetrc=False,
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001025 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1026 """Initialize an instance. Arguments:
1027 - host: hostname to connect to
1028 - port: port to connect to (default the standard NNTP port)
1029 - user: username to authenticate with
1030 - password: password to use with username
1031 - readermode: if true, send 'mode reader' command after
1032 connecting.
1033 - usenetrc: allow loading username and password from ~/.netrc file
1034 if not specified explicitly
1035 - timeout: timeout (in seconds) used for socket connections
1036
1037 readermode is sometimes necessary if you are connecting to an
1038 NNTP server on the local machine and intend to call
Ezio Melotti4969f702011-03-15 05:59:46 +02001039 reader-specific commands, such as `group'. If you get
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001040 unexpected NNTPPermanentErrors, you might need to set
1041 readermode.
1042 """
1043 self.host = host
1044 self.port = port
1045 self.sock = socket.create_connection((host, port), timeout)
1046 file = self.sock.makefile("rwb")
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001047 _NNTPBase.__init__(self, file, host,
1048 readermode, timeout)
1049 if user or usenetrc:
1050 self.login(user, password, usenetrc)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001051
1052 def _close(self):
1053 try:
1054 _NNTPBase._close(self)
1055 finally:
1056 self.sock.close()
1057
1058
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001059if _have_ssl:
1060 class NNTP_SSL(_NNTPBase):
1061
1062 def __init__(self, host, port=NNTP_SSL_PORT,
1063 user=None, password=None, ssl_context=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001064 readermode=None, usenetrc=False,
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001065 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1066 """This works identically to NNTP.__init__, except for the change
1067 in default port and the `ssl_context` argument for SSL connections.
1068 """
1069 self.sock = socket.create_connection((host, port), timeout)
1070 self.sock = _encrypt_on(self.sock, ssl_context)
1071 file = self.sock.makefile("rwb")
1072 _NNTPBase.__init__(self, file, host,
1073 readermode=readermode, timeout=timeout)
1074 if user or usenetrc:
1075 self.login(user, password, usenetrc)
1076
1077 def _close(self):
1078 try:
1079 _NNTPBase._close(self)
1080 finally:
1081 self.sock.close()
1082
1083 __all__.append("NNTP_SSL")
1084
1085
Neal Norwitzef679562002-11-14 02:19:44 +00001086# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +00001087if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001088 import argparse
1089 from email.utils import parsedate
1090
1091 parser = argparse.ArgumentParser(description="""\
1092 nntplib built-in demo - display the latest articles in a newsgroup""")
1093 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
1094 help='group to fetch messages from (default: %(default)s)')
1095 parser.add_argument('-s', '--server', default='news.gmane.org',
1096 help='NNTP server hostname (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001097 parser.add_argument('-p', '--port', default=-1, type=int,
1098 help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT))
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001099 parser.add_argument('-n', '--nb-articles', default=10, type=int,
1100 help='number of articles to fetch (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001101 parser.add_argument('-S', '--ssl', action='store_true', default=False,
1102 help='use NNTP over SSL')
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001103 args = parser.parse_args()
1104
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001105 port = args.port
1106 if not args.ssl:
1107 if port == -1:
1108 port = NNTP_PORT
1109 s = NNTP(host=args.server, port=port)
1110 else:
1111 if port == -1:
1112 port = NNTP_SSL_PORT
1113 s = NNTP_SSL(host=args.server, port=port)
1114
1115 caps = s.getcapabilities()
1116 if 'STARTTLS' in caps:
1117 s.starttls()
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001118 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001119 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001120
1121 def cut(s, lim):
1122 if len(s) > lim:
1123 s = s[:lim - 4] + "..."
1124 return s
1125
1126 first = str(int(last) - args.nb_articles + 1)
1127 resp, overviews = s.xover(first, last)
1128 for artnum, over in overviews:
1129 author = decode_header(over['from']).split('<', 1)[0]
1130 subject = decode_header(over['subject'])
1131 lines = int(over[':lines'])
1132 print("{:7} {:20} {:42} ({})".format(
1133 artnum, cut(author, 20), cut(subject, 42), lines)
1134 )
1135
1136 s.quit()