blob: 01d4303f4dae903c32f3c4ad997233a7004669e5 [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
74except ImportError:
75 _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
Barry Warsaw9dd78722000-02-10 20:25:53 +000088# Exceptions raised when an error or invalid response is received
89class NNTPError(Exception):
Tim Peters2344fae2001-01-15 00:50:52 +000090 """Base class for all nntplib exceptions"""
91 def __init__(self, *args):
Guido van Rossum68468eb2003-02-27 20:14:51 +000092 Exception.__init__(self, *args)
Tim Peters2344fae2001-01-15 00:50:52 +000093 try:
94 self.response = args[0]
95 except IndexError:
96 self.response = 'No response given'
Barry Warsaw9dd78722000-02-10 20:25:53 +000097
98class NNTPReplyError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000099 """Unexpected [123]xx reply"""
100 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000101
102class NNTPTemporaryError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000103 """4xx errors"""
104 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000105
106class NNTPPermanentError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000107 """5xx errors"""
108 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000109
110class NNTPProtocolError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000111 """Response does not begin with [1-5]"""
112 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000113
114class NNTPDataError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000115 """Error in response data"""
116 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000117
Tim Peters2344fae2001-01-15 00:50:52 +0000118
Guido van Rossumc629d341992-11-05 10:43:02 +0000119# Standard port used by NNTP servers
120NNTP_PORT = 119
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000121NNTP_SSL_PORT = 563
Guido van Rossumc629d341992-11-05 10:43:02 +0000122
123# Response numbers that are followed by additional text (e.g. article)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000124_LONGRESP = {
125 '100', # HELP
126 '101', # CAPABILITIES
127 '211', # LISTGROUP (also not multi-line with GROUP)
128 '215', # LIST
129 '220', # ARTICLE
130 '221', # HEAD, XHDR
131 '222', # BODY
132 '224', # OVER, XOVER
133 '225', # HDR
134 '230', # NEWNEWS
135 '231', # NEWGROUPS
136 '282', # XGTITLE
137}
Guido van Rossumc629d341992-11-05 10:43:02 +0000138
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000139# Default decoded value for LIST OVERVIEW.FMT if not supported
140_DEFAULT_OVERVIEW_FMT = [
141 "subject", "from", "date", "message-id", "references", ":bytes", ":lines"]
142
143# Alternative names allowed in LIST OVERVIEW.FMT response
144_OVERVIEW_FMT_ALTERNATIVES = {
145 'bytes': ':bytes',
146 'lines': ':lines',
147}
Guido van Rossumc629d341992-11-05 10:43:02 +0000148
149# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000150_CRLF = b'\r\n'
151
152GroupInfo = collections.namedtuple('GroupInfo',
153 ['group', 'last', 'first', 'flag'])
154
155ArticleInfo = collections.namedtuple('ArticleInfo',
156 ['number', 'message_id', 'lines'])
Guido van Rossumc629d341992-11-05 10:43:02 +0000157
158
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000159# Helper function(s)
160def decode_header(header_str):
161 """Takes an unicode string representing a munged header value
162 and decodes it as a (possibly non-ASCII) readable value."""
163 parts = []
164 for v, enc in _email_decode_header(header_str):
165 if isinstance(v, bytes):
166 parts.append(v.decode(enc or 'ascii'))
167 else:
168 parts.append(v)
R David Murray07ea53c2012-06-02 17:56:49 -0400169 return ''.join(parts)
Tim Peters2344fae2001-01-15 00:50:52 +0000170
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000171def _parse_overview_fmt(lines):
172 """Parse a list of string representing the response to LIST OVERVIEW.FMT
173 and return a list of header/metadata names.
174 Raises NNTPDataError if the response is not compliant
175 (cf. RFC 3977, section 8.4)."""
176 fmt = []
177 for line in lines:
178 if line[0] == ':':
179 # Metadata name (e.g. ":bytes")
180 name, _, suffix = line[1:].partition(':')
181 name = ':' + name
182 else:
183 # Header name (e.g. "Subject:" or "Xref:full")
184 name, _, suffix = line.partition(':')
185 name = name.lower()
186 name = _OVERVIEW_FMT_ALTERNATIVES.get(name, name)
187 # Should we do something with the suffix?
188 fmt.append(name)
189 defaults = _DEFAULT_OVERVIEW_FMT
190 if len(fmt) < len(defaults):
191 raise NNTPDataError("LIST OVERVIEW.FMT response too short")
192 if fmt[:len(defaults)] != defaults:
193 raise NNTPDataError("LIST OVERVIEW.FMT redefines default fields")
194 return fmt
195
196def _parse_overview(lines, fmt, data_process_func=None):
197 """Parse the response to a OVER or XOVER command according to the
198 overview format `fmt`."""
199 n_defaults = len(_DEFAULT_OVERVIEW_FMT)
200 overview = []
201 for line in lines:
202 fields = {}
203 article_number, *tokens = line.split('\t')
204 article_number = int(article_number)
205 for i, token in enumerate(tokens):
206 if i >= len(fmt):
207 # XXX should we raise an error? Some servers might not
208 # support LIST OVERVIEW.FMT and still return additional
209 # headers.
210 continue
211 field_name = fmt[i]
212 is_metadata = field_name.startswith(':')
213 if i >= n_defaults and not is_metadata:
214 # Non-default header names are included in full in the response
Antoine Pitrou4103bc02010-11-03 18:18:43 +0000215 # (unless the field is totally empty)
216 h = field_name + ": "
217 if token and token[:len(h)].lower() != h:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000218 raise NNTPDataError("OVER/XOVER response doesn't include "
219 "names of additional headers")
Antoine Pitrou4103bc02010-11-03 18:18:43 +0000220 token = token[len(h):] if token else None
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000221 fields[fmt[i]] = token
222 overview.append((article_number, fields))
223 return overview
224
225def _parse_datetime(date_str, time_str=None):
226 """Parse a pair of (date, time) strings, and return a datetime object.
227 If only the date is given, it is assumed to be date and time
228 concatenated together (e.g. response to the DATE command).
229 """
230 if time_str is None:
231 time_str = date_str[-6:]
232 date_str = date_str[:-6]
233 hours = int(time_str[:2])
234 minutes = int(time_str[2:4])
235 seconds = int(time_str[4:])
236 year = int(date_str[:-4])
237 month = int(date_str[-4:-2])
238 day = int(date_str[-2:])
239 # RFC 3977 doesn't say how to interpret 2-char years. Assume that
240 # there are no dates before 1970 on Usenet.
241 if year < 70:
242 year += 2000
243 elif year < 100:
244 year += 1900
245 return datetime.datetime(year, month, day, hours, minutes, seconds)
246
247def _unparse_datetime(dt, legacy=False):
248 """Format a date or datetime object as a pair of (date, time) strings
249 in the format required by the NEWNEWS and NEWGROUPS commands. If a
250 date object is passed, the time is assumed to be midnight (00h00).
251
252 The returned representation depends on the legacy flag:
253 * if legacy is False (the default):
254 date has the YYYYMMDD format and time the HHMMSS format
255 * if legacy is True:
256 date has the YYMMDD format and time the HHMMSS format.
257 RFC 3977 compliant servers should understand both formats; therefore,
258 legacy is only needed when talking to old servers.
259 """
260 if not isinstance(dt, datetime.datetime):
261 time_str = "000000"
262 else:
263 time_str = "{0.hour:02d}{0.minute:02d}{0.second:02d}".format(dt)
264 y = dt.year
265 if legacy:
266 y = y % 100
267 date_str = "{0:02d}{1.month:02d}{1.day:02d}".format(y, dt)
268 else:
269 date_str = "{0:04d}{1.month:02d}{1.day:02d}".format(y, dt)
270 return date_str, time_str
271
272
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000273if _have_ssl:
274
275 def _encrypt_on(sock, context):
276 """Wrap a socket in SSL/TLS. Arguments:
277 - sock: Socket to wrap
278 - context: SSL context to use for the encrypted connection
279 Returns:
280 - sock: New, encrypted socket.
281 """
282 # Generate a default SSL context if none was passed.
283 if context is None:
284 context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
285 # SSLv2 considered harmful.
286 context.options |= ssl.OP_NO_SSLv2
287 return context.wrap_socket(sock)
288
289
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000290# The classes themselves
291class _NNTPBase:
292 # UTF-8 is the character set for all NNTP commands and responses: they
293 # are automatically encoded (when sending) and decoded (and receiving)
294 # by this class.
295 # However, some multi-line data blocks can contain arbitrary bytes (for
296 # example, latin-1 or utf-16 data in the body of a message). Commands
297 # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
298 # data will therefore only accept and produce bytes objects.
299 # Furthermore, since there could be non-compliant servers out there,
300 # we use 'surrogateescape' as the error handler for fault tolerance
301 # and easy round-tripping. This could be useful for some applications
302 # (e.g. NNTP gateways).
303
304 encoding = 'utf-8'
305 errors = 'surrogateescape'
306
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000307 def __init__(self, file, host,
308 readermode=None, timeout=_GLOBAL_DEFAULT_TIMEOUT):
Tim Peters2344fae2001-01-15 00:50:52 +0000309 """Initialize an instance. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000310 - file: file-like object (open for read/write in binary mode)
Antoine Pitrou859c4ef2010-11-09 18:58:42 +0000311 - host: hostname of the server
Tim Peters2344fae2001-01-15 00:50:52 +0000312 - readermode: if true, send 'mode reader' command after
313 connecting.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000314 - timeout: timeout (in seconds) used for socket connections
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000315
Tim Peters2344fae2001-01-15 00:50:52 +0000316 readermode is sometimes necessary if you are connecting to an
317 NNTP server on the local machine and intend to call
Ezio Melotti42da6632011-03-15 05:18:48 +0200318 reader-specific commands, such as `group'. If you get
Tim Peters2344fae2001-01-15 00:50:52 +0000319 unexpected NNTPPermanentErrors, you might need to set
320 readermode.
321 """
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000322 self.host = host
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000323 self.file = file
Tim Peters2344fae2001-01-15 00:50:52 +0000324 self.debugging = 0
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000325 self.welcome = self._getresp()
Tim Petersdfb673b2001-01-16 07:12:46 +0000326
Antoine Pitrou71135622012-02-14 23:29:34 +0100327 # Inquire about capabilities (RFC 3977).
328 self._caps = None
329 self.getcapabilities()
330
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000331 # 'MODE READER' is sometimes necessary to enable 'reader' mode.
332 # However, the order in which 'MODE READER' and 'AUTHINFO' need to
333 # arrive differs between some NNTP servers. If _setreadermode() fails
334 # with an authorization failed error, it will set this to True;
335 # the login() routine will interpret that as a request to try again
336 # after performing its normal function.
Antoine Pitrou71135622012-02-14 23:29:34 +0100337 # Enable only if we're not already in READER mode anyway.
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000338 self.readermode_afterauth = False
Antoine Pitrou71135622012-02-14 23:29:34 +0100339 if readermode and 'READER' not in self._caps:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000340 self._setreadermode()
Antoine Pitrou71135622012-02-14 23:29:34 +0100341 if not self.readermode_afterauth:
342 # Capabilities might have changed after MODE READER
343 self._caps = None
344 self.getcapabilities()
Tim Petersdfb673b2001-01-16 07:12:46 +0000345
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000346 # RFC 4642 2.2.2: Both the client and the server MUST know if there is
347 # a TLS session active. A client MUST NOT attempt to start a TLS
348 # session if a TLS session is already active.
349 self.tls_on = False
350
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000351 # Log in and encryption setup order is left to subclasses.
352 self.authenticated = False
Guido van Rossumc629d341992-11-05 10:43:02 +0000353
Giampaolo Rodolà424298a2011-03-03 18:34:06 +0000354 def __enter__(self):
355 return self
356
357 def __exit__(self, *args):
358 is_connected = lambda: hasattr(self, "file")
359 if is_connected():
360 try:
361 self.quit()
Andrew Svetlov0832af62012-12-18 23:10:48 +0200362 except (OSError, EOFError):
Giampaolo Rodolà424298a2011-03-03 18:34:06 +0000363 pass
364 finally:
365 if is_connected():
366 self._close()
367
Tim Peters2344fae2001-01-15 00:50:52 +0000368 def getwelcome(self):
369 """Get the welcome message from the server
370 (this is read and squirreled away by __init__()).
371 If the response code is 200, posting is allowed;
372 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000373
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000374 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000375 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000376
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000377 def getcapabilities(self):
378 """Get the server capabilities, as read by __init__().
379 If the CAPABILITIES command is not supported, an empty dict is
380 returned."""
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000381 if self._caps is None:
382 self.nntp_version = 1
383 self.nntp_implementation = None
384 try:
385 resp, caps = self.capabilities()
Antoine Pitrou54411c12012-02-12 19:14:17 +0100386 except (NNTPPermanentError, NNTPTemporaryError):
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000387 # Server doesn't support capabilities
388 self._caps = {}
389 else:
390 self._caps = caps
391 if 'VERSION' in caps:
392 # The server can advertise several supported versions,
393 # choose the highest.
394 self.nntp_version = max(map(int, caps['VERSION']))
395 if 'IMPLEMENTATION' in caps:
396 self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000397 return self._caps
398
Tim Peters2344fae2001-01-15 00:50:52 +0000399 def set_debuglevel(self, level):
400 """Set the debugging level. Argument 'level' means:
401 0: no debugging output (default)
402 1: print commands and responses but not body text etc.
403 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000404
Tim Peters2344fae2001-01-15 00:50:52 +0000405 self.debugging = level
406 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000407
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000408 def _putline(self, line):
409 """Internal: send one line to the server, appending CRLF.
410 The `line` must be a bytes-like object."""
411 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000412 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000413 self.file.write(line)
414 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000415
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000416 def _putcmd(self, line):
417 """Internal: send one command to the server (through _putline()).
418 The `line` must be an unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000419 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000420 line = line.encode(self.encoding, self.errors)
421 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000422
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000423 def _getline(self, strip_crlf=True):
424 """Internal: return one line from the server, stripping _CRLF.
425 Raise EOFError if the connection is closed.
426 Returns a bytes object."""
Tim Peters2344fae2001-01-15 00:50:52 +0000427 line = self.file.readline()
428 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000429 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000430 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000431 if strip_crlf:
432 if line[-2:] == _CRLF:
433 line = line[:-2]
434 elif line[-1:] in _CRLF:
435 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000436 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000437
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000438 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000439 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000440 Raise various errors if the response indicates an error.
441 Returns an unicode string."""
442 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000443 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000444 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000445 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000446 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000447 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000448 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000449 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000450 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000451 raise NNTPProtocolError(resp)
452 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000453
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000454 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000455 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000456 Raise various errors if the response indicates an error.
457
458 Returns a (response, lines) tuple where `response` is an unicode
459 string and `lines` is a list of bytes objects.
460 If `file` is a file-like object, it must be open in binary mode.
461 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000462
463 openedFile = None
464 try:
465 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000466 if isinstance(file, (str, bytes)):
467 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000468
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000469 resp = self._getresp()
470 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000471 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000472
473 lines = []
474 if file is not None:
475 # XXX lines = None instead?
476 terminators = (b'.' + _CRLF, b'.\n')
477 while 1:
478 line = self._getline(False)
479 if line in terminators:
480 break
481 if line.startswith(b'..'):
482 line = line[1:]
483 file.write(line)
484 else:
485 terminator = b'.'
486 while 1:
487 line = self._getline()
488 if line == terminator:
489 break
490 if line.startswith(b'..'):
491 line = line[1:]
492 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000493 finally:
494 # If this method created the file, then it must close it
495 if openedFile:
496 openedFile.close()
497
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000498 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000499
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000500 def _shortcmd(self, line):
501 """Internal: send a command and get the response.
502 Same return value as _getresp()."""
503 self._putcmd(line)
504 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000505
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000506 def _longcmd(self, line, file=None):
507 """Internal: send a command and get the response plus following text.
508 Same return value as _getlongresp()."""
509 self._putcmd(line)
510 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000511
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000512 def _longcmdstring(self, line, file=None):
513 """Internal: send a command and get the response plus following text.
514 Same as _longcmd() and _getlongresp(), except that the returned `lines`
515 are unicode strings rather than bytes objects.
516 """
517 self._putcmd(line)
518 resp, list = self._getlongresp(file)
519 return resp, [line.decode(self.encoding, self.errors)
520 for line in list]
521
522 def _getoverviewfmt(self):
523 """Internal: get the overview format. Queries the server if not
524 already done, else returns the cached value."""
525 try:
526 return self._cachedoverviewfmt
527 except AttributeError:
528 pass
529 try:
530 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
531 except NNTPPermanentError:
532 # Not supported by server?
533 fmt = _DEFAULT_OVERVIEW_FMT[:]
534 else:
535 fmt = _parse_overview_fmt(lines)
536 self._cachedoverviewfmt = fmt
537 return fmt
538
539 def _grouplist(self, lines):
540 # Parse lines into "group last first flag"
541 return [GroupInfo(*line.split()) for line in lines]
542
543 def capabilities(self):
544 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000545 Return:
546 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000547 - caps: a dictionary mapping capability names to lists of tokens
548 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
549 """
550 caps = {}
551 resp, lines = self._longcmdstring("CAPABILITIES")
552 for line in lines:
553 name, *tokens = line.split()
554 caps[name] = tokens
555 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000556
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000557 def newgroups(self, date, *, file=None):
558 """Process a NEWGROUPS command. Arguments:
559 - date: a date or datetime object
560 Return:
561 - resp: server response if successful
562 - list: list of newsgroup names
563 """
564 if not isinstance(date, (datetime.date, datetime.date)):
565 raise TypeError(
566 "the date parameter must be a date or datetime object, "
567 "not '{:40}'".format(date.__class__.__name__))
568 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
569 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
570 resp, lines = self._longcmdstring(cmd, file)
571 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000572
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000573 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000574 """Process a NEWNEWS command. Arguments:
575 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000576 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000577 Return:
578 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000579 - list: list of message ids
580 """
581 if not isinstance(date, (datetime.date, datetime.date)):
582 raise TypeError(
583 "the date parameter must be a date or datetime object, "
584 "not '{:40}'".format(date.__class__.__name__))
585 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
586 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
587 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000588
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000589 def list(self, group_pattern=None, *, file=None):
590 """Process a LIST or LIST ACTIVE command. Arguments:
591 - group_pattern: a pattern indicating which groups to query
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000592 - file: Filename string or file object to store the result in
593 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000594 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000595 - list: list of (group, last, first, flag) (strings)
596 """
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000597 if group_pattern is not None:
598 command = 'LIST ACTIVE ' + group_pattern
599 else:
600 command = 'LIST'
601 resp, lines = self._longcmdstring(command, file)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000602 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000603
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000604 def _getdescriptions(self, group_pattern, return_all):
605 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
606 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
607 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
608 if not resp.startswith('215'):
609 # Now the deprecated XGTITLE. This either raises an error
610 # or succeeds with the same output structure as LIST
611 # NEWSGROUPS.
612 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
613 groups = {}
614 for raw_line in lines:
615 match = line_pat.search(raw_line.strip())
616 if match:
617 name, desc = match.group(1, 2)
618 if not return_all:
619 return desc
620 groups[name] = desc
621 if return_all:
622 return resp, groups
623 else:
624 # Nothing found
625 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000626
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000627 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000628 """Get a description for a single group. If more than one
629 group matches ('group' is a pattern), return the first. If no
630 group matches, return an empty string.
631
632 This elides the response code from the server, since it can
633 only be '215' or '285' (for xgtitle) anyway. If the response
634 code is needed, use the 'descriptions' method.
635
636 NOTE: This neither checks for a wildcard in 'group' nor does
637 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000638 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000639
640 def descriptions(self, group_pattern):
641 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000642 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000643
Tim Peters2344fae2001-01-15 00:50:52 +0000644 def group(self, name):
645 """Process a GROUP command. Argument:
646 - group: the group name
647 Returns:
648 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000649 - count: number of articles
650 - first: first article number
651 - last: last article number
652 - name: the group name
653 """
654 resp = self._shortcmd('GROUP ' + name)
655 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000656 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000657 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000658 count = first = last = 0
659 n = len(words)
660 if n > 1:
661 count = words[1]
662 if n > 2:
663 first = words[2]
664 if n > 3:
665 last = words[3]
666 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000667 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000668 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000669
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000670 def help(self, *, file=None):
671 """Process a HELP command. Argument:
672 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000673 Returns:
674 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000675 - list: list of strings returned by the server in response to the
676 HELP command
677 """
678 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000679
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000680 def _statparse(self, resp):
681 """Internal: parse the response line of a STAT, NEXT, LAST,
682 ARTICLE, HEAD or BODY command."""
683 if not resp.startswith('22'):
684 raise NNTPReplyError(resp)
685 words = resp.split()
686 art_num = int(words[1])
687 message_id = words[2]
688 return resp, art_num, message_id
689
690 def _statcmd(self, line):
691 """Internal: process a STAT, NEXT or LAST command."""
692 resp = self._shortcmd(line)
693 return self._statparse(resp)
694
695 def stat(self, message_spec=None):
696 """Process a STAT command. Argument:
697 - message_spec: article number or message id (if not specified,
698 the current article is selected)
699 Returns:
700 - resp: server response if successful
701 - art_num: the article number
702 - message_id: the message id
703 """
704 if message_spec:
705 return self._statcmd('STAT {0}'.format(message_spec))
706 else:
707 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000708
Tim Peters2344fae2001-01-15 00:50:52 +0000709 def next(self):
710 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000711 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000712
Tim Peters2344fae2001-01-15 00:50:52 +0000713 def last(self):
714 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000715 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000716
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000717 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000718 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000719 resp, lines = self._longcmd(line, file)
720 resp, art_num, message_id = self._statparse(resp)
721 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000722
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000723 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000724 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000725 - message_spec: article number or message id
726 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000727 Returns:
728 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000729 - ArticleInfo: (article number, message id, list of header lines)
730 """
731 if message_spec is not None:
732 cmd = 'HEAD {0}'.format(message_spec)
733 else:
734 cmd = 'HEAD'
735 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000736
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000737 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000738 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000739 - message_spec: article number or message id
740 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000741 Returns:
742 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000743 - ArticleInfo: (article number, message id, list of body lines)
744 """
745 if message_spec is not None:
746 cmd = 'BODY {0}'.format(message_spec)
747 else:
748 cmd = 'BODY'
749 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000750
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000751 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000752 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000753 - message_spec: article number or message id
754 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000755 Returns:
756 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000757 - ArticleInfo: (article number, message id, list of article lines)
758 """
759 if message_spec is not None:
760 cmd = 'ARTICLE {0}'.format(message_spec)
761 else:
762 cmd = 'ARTICLE'
763 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000764
Tim Peters2344fae2001-01-15 00:50:52 +0000765 def slave(self):
766 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000767 - resp: server response if successful
768 """
769 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000770
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000771 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000772 """Process an XHDR command (optional server extension). Arguments:
773 - hdr: the header type (e.g. 'subject')
774 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000775 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000776 Returns:
777 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000778 - list: list of (nr, value) strings
779 """
780 pat = re.compile('^([0-9]+) ?(.*)\n?')
781 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
782 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000783 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000784 return m.group(1, 2) if m else line
785 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000786
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000787 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000788 """Process an XOVER command (optional server extension) Arguments:
789 - start: start of range
790 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000791 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000792 Returns:
793 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000794 - list: list of dicts containing the response fields
795 """
796 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
797 file)
798 fmt = self._getoverviewfmt()
799 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000800
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000801 def over(self, message_spec, *, file=None):
802 """Process an OVER command. If the command isn't supported, fall
803 back to XOVER. Arguments:
804 - message_spec:
805 - either a message id, indicating the article to fetch
806 information about
807 - or a (start, end) tuple, indicating a range of article numbers;
808 if end is None, information up to the newest message will be
809 retrieved
810 - or None, indicating the current article number must be used
811 - file: Filename string or file object to store the result in
812 Returns:
813 - resp: server response if successful
814 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000815
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000816 NOTE: the "message id" form isn't supported by XOVER
817 """
818 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
819 if isinstance(message_spec, (tuple, list)):
820 start, end = message_spec
821 cmd += ' {0}-{1}'.format(start, end or '')
822 elif message_spec is not None:
823 cmd = cmd + ' ' + message_spec
824 resp, lines = self._longcmdstring(cmd, file)
825 fmt = self._getoverviewfmt()
826 return resp, _parse_overview(lines, fmt)
827
828 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000829 """Process an XGTITLE command (optional server extension) Arguments:
830 - group: group name wildcard (i.e. news.*)
831 Returns:
832 - resp: server response if successful
833 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000834 warnings.warn("The XGTITLE extension is not actively used, "
835 "use descriptions() instead",
Florent Xicluna67317752011-12-10 11:07:42 +0100836 DeprecationWarning, 2)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000837 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
838 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000839 lines = []
840 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000841 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000842 if match:
843 lines.append(match.group(1, 2))
844 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000845
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000846 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000847 """Process an XPATH command (optional server extension) Arguments:
848 - id: Message id of article
849 Returns:
850 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000851 path: directory path to article
852 """
853 warnings.warn("The XPATH extension is not actively used",
Florent Xicluna67317752011-12-10 11:07:42 +0100854 DeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000855
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000856 resp = self._shortcmd('XPATH {0}'.format(id))
857 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000858 raise NNTPReplyError(resp)
859 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000860 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000861 except ValueError:
862 raise NNTPReplyError(resp)
863 else:
864 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000865
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000866 def date(self):
867 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000868 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000869 - resp: server response if successful
870 - date: datetime object
871 """
872 resp = self._shortcmd("DATE")
873 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000874 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000875 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000876 if len(elem) != 2:
877 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000878 date = elem[1]
879 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000880 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000881 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000882
Christian Heimes933238a2008-11-05 19:44:21 +0000883 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000884 resp = self._shortcmd(command)
885 # Raises a specific exception if posting is not allowed
886 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000887 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000888 if isinstance(f, (bytes, bytearray)):
889 f = f.splitlines()
890 # We don't use _putline() because:
891 # - we don't want additional CRLF if the file or iterable is already
892 # in the right format
893 # - we don't want a spurious flush() after each line is written
894 for line in f:
895 if not line.endswith(_CRLF):
896 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000897 if line.startswith(b'.'):
898 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000899 self.file.write(line)
900 self.file.write(b".\r\n")
901 self.file.flush()
902 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000903
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000904 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000905 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000906 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000907 Returns:
908 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000909 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000910
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000911 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000912 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000913 - message_id: message-id of the article
914 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000915 Returns:
916 - resp: server response if successful
917 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000918 return self._post('IHAVE {0}'.format(message_id), data)
919
920 def _close(self):
921 self.file.close()
922 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000923
Tim Peters2344fae2001-01-15 00:50:52 +0000924 def quit(self):
925 """Process a QUIT command and close the socket. Returns:
926 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000927 try:
928 resp = self._shortcmd('QUIT')
929 finally:
930 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000931 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000932
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000933 def login(self, user=None, password=None, usenetrc=True):
934 if self.authenticated:
935 raise ValueError("Already logged in.")
936 if not user and not usenetrc:
937 raise ValueError(
938 "At least one of `user` and `usenetrc` must be specified")
939 # If no login/password was specified but netrc was requested,
940 # try to get them from ~/.netrc
941 # Presume that if .netrc has an entry, NNRP authentication is required.
942 try:
943 if usenetrc and not user:
944 import netrc
945 credentials = netrc.netrc()
946 auth = credentials.authenticators(self.host)
947 if auth:
948 user = auth[0]
949 password = auth[2]
Andrew Svetlovf7a17b42012-12-25 16:47:37 +0200950 except OSError:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000951 pass
952 # Perform NNTP authentication if needed.
953 if not user:
954 return
955 resp = self._shortcmd('authinfo user ' + user)
956 if resp.startswith('381'):
957 if not password:
958 raise NNTPReplyError(resp)
959 else:
960 resp = self._shortcmd('authinfo pass ' + password)
961 if not resp.startswith('281'):
962 raise NNTPPermanentError(resp)
Antoine Pitrou54411c12012-02-12 19:14:17 +0100963 # Capabilities might have changed after login
964 self._caps = None
965 self.getcapabilities()
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000966 # Attempt to send mode reader if it was requested after login.
Antoine Pitrou71135622012-02-14 23:29:34 +0100967 # Only do so if we're not in reader mode already.
968 if self.readermode_afterauth and 'READER' not in self._caps:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000969 self._setreadermode()
Antoine Pitrou71135622012-02-14 23:29:34 +0100970 # Capabilities might have changed after MODE READER
971 self._caps = None
972 self.getcapabilities()
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000973
974 def _setreadermode(self):
975 try:
976 self.welcome = self._shortcmd('mode reader')
977 except NNTPPermanentError:
978 # Error 5xx, probably 'not implemented'
979 pass
980 except NNTPTemporaryError as e:
981 if e.response.startswith('480'):
982 # Need authorization before 'mode reader'
983 self.readermode_afterauth = True
984 else:
985 raise
986
987 if _have_ssl:
988 def starttls(self, context=None):
989 """Process a STARTTLS command. Arguments:
990 - context: SSL context to use for the encrypted connection
991 """
992 # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if
993 # a TLS session already exists.
994 if self.tls_on:
995 raise ValueError("TLS is already enabled.")
996 if self.authenticated:
997 raise ValueError("TLS cannot be started after authentication.")
998 resp = self._shortcmd('STARTTLS')
999 if resp.startswith('382'):
1000 self.file.close()
1001 self.sock = _encrypt_on(self.sock, context)
1002 self.file = self.sock.makefile("rwb")
1003 self.tls_on = True
1004 # Capabilities may change after TLS starts up, so ask for them
1005 # again.
1006 self._caps = None
1007 self.getcapabilities()
1008 else:
1009 raise NNTPError("TLS failed to start.")
1010
Guido van Rossume2ed9df1997-08-26 23:26:18 +00001011
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001012class NNTP(_NNTPBase):
1013
1014 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001015 readermode=None, usenetrc=False,
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001016 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1017 """Initialize an instance. Arguments:
1018 - host: hostname to connect to
1019 - port: port to connect to (default the standard NNTP port)
1020 - user: username to authenticate with
1021 - password: password to use with username
1022 - readermode: if true, send 'mode reader' command after
1023 connecting.
1024 - usenetrc: allow loading username and password from ~/.netrc file
1025 if not specified explicitly
1026 - timeout: timeout (in seconds) used for socket connections
1027
1028 readermode is sometimes necessary if you are connecting to an
1029 NNTP server on the local machine and intend to call
Ezio Melotti4969f702011-03-15 05:59:46 +02001030 reader-specific commands, such as `group'. If you get
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001031 unexpected NNTPPermanentErrors, you might need to set
1032 readermode.
1033 """
1034 self.host = host
1035 self.port = port
1036 self.sock = socket.create_connection((host, port), timeout)
1037 file = self.sock.makefile("rwb")
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001038 _NNTPBase.__init__(self, file, host,
1039 readermode, timeout)
1040 if user or usenetrc:
1041 self.login(user, password, usenetrc)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001042
1043 def _close(self):
1044 try:
1045 _NNTPBase._close(self)
1046 finally:
1047 self.sock.close()
1048
1049
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001050if _have_ssl:
1051 class NNTP_SSL(_NNTPBase):
1052
1053 def __init__(self, host, port=NNTP_SSL_PORT,
1054 user=None, password=None, ssl_context=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001055 readermode=None, usenetrc=False,
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001056 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1057 """This works identically to NNTP.__init__, except for the change
1058 in default port and the `ssl_context` argument for SSL connections.
1059 """
1060 self.sock = socket.create_connection((host, port), timeout)
1061 self.sock = _encrypt_on(self.sock, ssl_context)
1062 file = self.sock.makefile("rwb")
1063 _NNTPBase.__init__(self, file, host,
1064 readermode=readermode, timeout=timeout)
1065 if user or usenetrc:
1066 self.login(user, password, usenetrc)
1067
1068 def _close(self):
1069 try:
1070 _NNTPBase._close(self)
1071 finally:
1072 self.sock.close()
1073
1074 __all__.append("NNTP_SSL")
1075
1076
Neal Norwitzef679562002-11-14 02:19:44 +00001077# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +00001078if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001079 import argparse
1080 from email.utils import parsedate
1081
1082 parser = argparse.ArgumentParser(description="""\
1083 nntplib built-in demo - display the latest articles in a newsgroup""")
1084 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
1085 help='group to fetch messages from (default: %(default)s)')
1086 parser.add_argument('-s', '--server', default='news.gmane.org',
1087 help='NNTP server hostname (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001088 parser.add_argument('-p', '--port', default=-1, type=int,
1089 help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT))
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001090 parser.add_argument('-n', '--nb-articles', default=10, type=int,
1091 help='number of articles to fetch (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001092 parser.add_argument('-S', '--ssl', action='store_true', default=False,
1093 help='use NNTP over SSL')
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001094 args = parser.parse_args()
1095
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001096 port = args.port
1097 if not args.ssl:
1098 if port == -1:
1099 port = NNTP_PORT
1100 s = NNTP(host=args.server, port=port)
1101 else:
1102 if port == -1:
1103 port = NNTP_SSL_PORT
1104 s = NNTP_SSL(host=args.server, port=port)
1105
1106 caps = s.getcapabilities()
1107 if 'STARTTLS' in caps:
1108 s.starttls()
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001109 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001110 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001111
1112 def cut(s, lim):
1113 if len(s) > lim:
1114 s = s[:lim - 4] + "..."
1115 return s
1116
1117 first = str(int(last) - args.nb_articles + 1)
1118 resp, overviews = s.xover(first, last)
1119 for artnum, over in overviews:
1120 author = decode_header(over['from']).split('<', 1)[0]
1121 subject = decode_header(over['subject'])
1122 lines = int(over[':lines'])
1123 print("{:7} {:20} {:42} ({})".format(
1124 artnum, cut(author, 20), cut(subject, 42), lines)
1125 )
1126
1127 s.quit()