blob: 1055d065108a507ed0ffe9ce3a2f7e4adc32703a [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 Pitrou69ab9512010-09-29 15:03:40 +000072from email.header import decode_header as _email_decode_header
73from socket import _GLOBAL_DEFAULT_TIMEOUT
74
75__all__ = ["NNTP",
76 "NNTPReplyError", "NNTPTemporaryError", "NNTPPermanentError",
77 "NNTPProtocolError", "NNTPDataError",
78 "decode_header",
79 ]
Tim Peters2344fae2001-01-15 00:50:52 +000080
Barry Warsaw9dd78722000-02-10 20:25:53 +000081# Exceptions raised when an error or invalid response is received
82class NNTPError(Exception):
Tim Peters2344fae2001-01-15 00:50:52 +000083 """Base class for all nntplib exceptions"""
84 def __init__(self, *args):
Guido van Rossum68468eb2003-02-27 20:14:51 +000085 Exception.__init__(self, *args)
Tim Peters2344fae2001-01-15 00:50:52 +000086 try:
87 self.response = args[0]
88 except IndexError:
89 self.response = 'No response given'
Barry Warsaw9dd78722000-02-10 20:25:53 +000090
91class NNTPReplyError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000092 """Unexpected [123]xx reply"""
93 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000094
95class NNTPTemporaryError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000096 """4xx errors"""
97 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +000098
99class NNTPPermanentError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000100 """5xx errors"""
101 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000102
103class NNTPProtocolError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000104 """Response does not begin with [1-5]"""
105 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000106
107class NNTPDataError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000108 """Error in response data"""
109 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000110
Tim Peters2344fae2001-01-15 00:50:52 +0000111
Guido van Rossumc629d341992-11-05 10:43:02 +0000112# Standard port used by NNTP servers
113NNTP_PORT = 119
114
115
116# Response numbers that are followed by additional text (e.g. article)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000117_LONGRESP = {
118 '100', # HELP
119 '101', # CAPABILITIES
120 '211', # LISTGROUP (also not multi-line with GROUP)
121 '215', # LIST
122 '220', # ARTICLE
123 '221', # HEAD, XHDR
124 '222', # BODY
125 '224', # OVER, XOVER
126 '225', # HDR
127 '230', # NEWNEWS
128 '231', # NEWGROUPS
129 '282', # XGTITLE
130}
Guido van Rossumc629d341992-11-05 10:43:02 +0000131
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000132# Default decoded value for LIST OVERVIEW.FMT if not supported
133_DEFAULT_OVERVIEW_FMT = [
134 "subject", "from", "date", "message-id", "references", ":bytes", ":lines"]
135
136# Alternative names allowed in LIST OVERVIEW.FMT response
137_OVERVIEW_FMT_ALTERNATIVES = {
138 'bytes': ':bytes',
139 'lines': ':lines',
140}
Guido van Rossumc629d341992-11-05 10:43:02 +0000141
142# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000143_CRLF = b'\r\n'
144
145GroupInfo = collections.namedtuple('GroupInfo',
146 ['group', 'last', 'first', 'flag'])
147
148ArticleInfo = collections.namedtuple('ArticleInfo',
149 ['number', 'message_id', 'lines'])
Guido van Rossumc629d341992-11-05 10:43:02 +0000150
151
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000152# Helper function(s)
153def decode_header(header_str):
154 """Takes an unicode string representing a munged header value
155 and decodes it as a (possibly non-ASCII) readable value."""
156 parts = []
157 for v, enc in _email_decode_header(header_str):
158 if isinstance(v, bytes):
159 parts.append(v.decode(enc or 'ascii'))
160 else:
161 parts.append(v)
162 return ' '.join(parts)
Tim Peters2344fae2001-01-15 00:50:52 +0000163
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000164def _parse_overview_fmt(lines):
165 """Parse a list of string representing the response to LIST OVERVIEW.FMT
166 and return a list of header/metadata names.
167 Raises NNTPDataError if the response is not compliant
168 (cf. RFC 3977, section 8.4)."""
169 fmt = []
170 for line in lines:
171 if line[0] == ':':
172 # Metadata name (e.g. ":bytes")
173 name, _, suffix = line[1:].partition(':')
174 name = ':' + name
175 else:
176 # Header name (e.g. "Subject:" or "Xref:full")
177 name, _, suffix = line.partition(':')
178 name = name.lower()
179 name = _OVERVIEW_FMT_ALTERNATIVES.get(name, name)
180 # Should we do something with the suffix?
181 fmt.append(name)
182 defaults = _DEFAULT_OVERVIEW_FMT
183 if len(fmt) < len(defaults):
184 raise NNTPDataError("LIST OVERVIEW.FMT response too short")
185 if fmt[:len(defaults)] != defaults:
186 raise NNTPDataError("LIST OVERVIEW.FMT redefines default fields")
187 return fmt
188
189def _parse_overview(lines, fmt, data_process_func=None):
190 """Parse the response to a OVER or XOVER command according to the
191 overview format `fmt`."""
192 n_defaults = len(_DEFAULT_OVERVIEW_FMT)
193 overview = []
194 for line in lines:
195 fields = {}
196 article_number, *tokens = line.split('\t')
197 article_number = int(article_number)
198 for i, token in enumerate(tokens):
199 if i >= len(fmt):
200 # XXX should we raise an error? Some servers might not
201 # support LIST OVERVIEW.FMT and still return additional
202 # headers.
203 continue
204 field_name = fmt[i]
205 is_metadata = field_name.startswith(':')
206 if i >= n_defaults and not is_metadata:
207 # Non-default header names are included in full in the response
208 h = field_name + ":"
209 if token[:len(h)].lower() != h:
210 raise NNTPDataError("OVER/XOVER response doesn't include "
211 "names of additional headers")
212 token = token[len(h):].lstrip(" ")
213 fields[fmt[i]] = token
214 overview.append((article_number, fields))
215 return overview
216
217def _parse_datetime(date_str, time_str=None):
218 """Parse a pair of (date, time) strings, and return a datetime object.
219 If only the date is given, it is assumed to be date and time
220 concatenated together (e.g. response to the DATE command).
221 """
222 if time_str is None:
223 time_str = date_str[-6:]
224 date_str = date_str[:-6]
225 hours = int(time_str[:2])
226 minutes = int(time_str[2:4])
227 seconds = int(time_str[4:])
228 year = int(date_str[:-4])
229 month = int(date_str[-4:-2])
230 day = int(date_str[-2:])
231 # RFC 3977 doesn't say how to interpret 2-char years. Assume that
232 # there are no dates before 1970 on Usenet.
233 if year < 70:
234 year += 2000
235 elif year < 100:
236 year += 1900
237 return datetime.datetime(year, month, day, hours, minutes, seconds)
238
239def _unparse_datetime(dt, legacy=False):
240 """Format a date or datetime object as a pair of (date, time) strings
241 in the format required by the NEWNEWS and NEWGROUPS commands. If a
242 date object is passed, the time is assumed to be midnight (00h00).
243
244 The returned representation depends on the legacy flag:
245 * if legacy is False (the default):
246 date has the YYYYMMDD format and time the HHMMSS format
247 * if legacy is True:
248 date has the YYMMDD format and time the HHMMSS format.
249 RFC 3977 compliant servers should understand both formats; therefore,
250 legacy is only needed when talking to old servers.
251 """
252 if not isinstance(dt, datetime.datetime):
253 time_str = "000000"
254 else:
255 time_str = "{0.hour:02d}{0.minute:02d}{0.second:02d}".format(dt)
256 y = dt.year
257 if legacy:
258 y = y % 100
259 date_str = "{0:02d}{1.month:02d}{1.day:02d}".format(y, dt)
260 else:
261 date_str = "{0:04d}{1.month:02d}{1.day:02d}".format(y, dt)
262 return date_str, time_str
263
264
265# The classes themselves
266class _NNTPBase:
267 # UTF-8 is the character set for all NNTP commands and responses: they
268 # are automatically encoded (when sending) and decoded (and receiving)
269 # by this class.
270 # However, some multi-line data blocks can contain arbitrary bytes (for
271 # example, latin-1 or utf-16 data in the body of a message). Commands
272 # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
273 # data will therefore only accept and produce bytes objects.
274 # Furthermore, since there could be non-compliant servers out there,
275 # we use 'surrogateescape' as the error handler for fault tolerance
276 # and easy round-tripping. This could be useful for some applications
277 # (e.g. NNTP gateways).
278
279 encoding = 'utf-8'
280 errors = 'surrogateescape'
281
282 def __init__(self, file, user=None, password=None,
283 readermode=None, usenetrc=True,
284 timeout=_GLOBAL_DEFAULT_TIMEOUT):
Tim Peters2344fae2001-01-15 00:50:52 +0000285 """Initialize an instance. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000286 - file: file-like object (open for read/write in binary mode)
Tim Peters2344fae2001-01-15 00:50:52 +0000287 - user: username to authenticate with
288 - password: password to use with username
289 - readermode: if true, send 'mode reader' command after
290 connecting.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000291 - usenetrc: allow loading username and password from ~/.netrc file
292 if not specified explicitly
293 - timeout: timeout (in seconds) used for socket connections
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000294
Tim Peters2344fae2001-01-15 00:50:52 +0000295 readermode is sometimes necessary if you are connecting to an
296 NNTP server on the local machine and intend to call
297 reader-specific comamnds, such as `group'. If you get
298 unexpected NNTPPermanentErrors, you might need to set
299 readermode.
300 """
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000301 self.file = file
Tim Peters2344fae2001-01-15 00:50:52 +0000302 self.debugging = 0
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000303 self.welcome = self._getresp()
Tim Petersdfb673b2001-01-16 07:12:46 +0000304
Thomas Wouters47adcba2001-01-16 06:35:14 +0000305 # 'mode reader' is sometimes necessary to enable 'reader' mode.
Tim Petersdfb673b2001-01-16 07:12:46 +0000306 # However, the order in which 'mode reader' and 'authinfo' need to
Thomas Wouters47adcba2001-01-16 06:35:14 +0000307 # arrive differs between some NNTP servers. Try to send
308 # 'mode reader', and if it fails with an authorization failed
309 # error, try again after sending authinfo.
310 readermode_afterauth = 0
Tim Peters2344fae2001-01-15 00:50:52 +0000311 if readermode:
312 try:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000313 self.welcome = self._shortcmd('mode reader')
Tim Peters2344fae2001-01-15 00:50:52 +0000314 except NNTPPermanentError:
315 # error 500, probably 'not implemented'
316 pass
Guido van Rossumb940e112007-01-10 16:19:56 +0000317 except NNTPTemporaryError as e:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000318 if user and e.response.startswith('480'):
Thomas Wouters47adcba2001-01-16 06:35:14 +0000319 # Need authorization before 'mode reader'
320 readermode_afterauth = 1
321 else:
322 raise
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000323 # If no login/password was specified, try to get them from ~/.netrc
324 # Presume that if .netc has an entry, NNRP authentication is required.
Eric S. Raymond782d9402002-11-17 17:53:12 +0000325 try:
Martin v. Löwis9513e342004-08-03 14:36:32 +0000326 if usenetrc and not user:
Eric S. Raymond782d9402002-11-17 17:53:12 +0000327 import netrc
328 credentials = netrc.netrc()
329 auth = credentials.authenticators(host)
330 if auth:
331 user = auth[0]
332 password = auth[2]
333 except IOError:
334 pass
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000335 # Perform NNTP authentication if needed.
Tim Peters2344fae2001-01-15 00:50:52 +0000336 if user:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000337 resp = self._shortcmd('authinfo user '+user)
338 if resp.startswith('381'):
Tim Peters2344fae2001-01-15 00:50:52 +0000339 if not password:
340 raise NNTPReplyError(resp)
341 else:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000342 resp = self._shortcmd(
Tim Peters2344fae2001-01-15 00:50:52 +0000343 'authinfo pass '+password)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000344 if not resp.startswith('281'):
Tim Peters2344fae2001-01-15 00:50:52 +0000345 raise NNTPPermanentError(resp)
Thomas Wouters47adcba2001-01-16 06:35:14 +0000346 if readermode_afterauth:
347 try:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000348 self.welcome = self._shortcmd('mode reader')
Thomas Wouters47adcba2001-01-16 06:35:14 +0000349 except NNTPPermanentError:
350 # error 500, probably 'not implemented'
351 pass
Tim Petersdfb673b2001-01-16 07:12:46 +0000352
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000353 # Inquire about capabilities (RFC 3977)
354 self.nntp_version = 1
355 try:
356 resp, caps = self.capabilities()
357 except NNTPPermanentError:
358 # Server doesn't support capabilities
359 self._caps = {}
360 else:
361 self._caps = caps
362 if 'VERSION' in caps:
363 self.nntp_version = int(caps['VERSION'][0])
Guido van Rossumc629d341992-11-05 10:43:02 +0000364
Tim Peters2344fae2001-01-15 00:50:52 +0000365 def getwelcome(self):
366 """Get the welcome message from the server
367 (this is read and squirreled away by __init__()).
368 If the response code is 200, posting is allowed;
369 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000370
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000371 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000372 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000373
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000374 def getcapabilities(self):
375 """Get the server capabilities, as read by __init__().
376 If the CAPABILITIES command is not supported, an empty dict is
377 returned."""
378 return self._caps
379
Tim Peters2344fae2001-01-15 00:50:52 +0000380 def set_debuglevel(self, level):
381 """Set the debugging level. Argument 'level' means:
382 0: no debugging output (default)
383 1: print commands and responses but not body text etc.
384 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000385
Tim Peters2344fae2001-01-15 00:50:52 +0000386 self.debugging = level
387 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000388
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000389 def _putline(self, line):
390 """Internal: send one line to the server, appending CRLF.
391 The `line` must be a bytes-like object."""
392 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000393 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000394 self.file.write(line)
395 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000396
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000397 def _putcmd(self, line):
398 """Internal: send one command to the server (through _putline()).
399 The `line` must be an unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000400 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000401 line = line.encode(self.encoding, self.errors)
402 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000403
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000404 def _getline(self, strip_crlf=True):
405 """Internal: return one line from the server, stripping _CRLF.
406 Raise EOFError if the connection is closed.
407 Returns a bytes object."""
Tim Peters2344fae2001-01-15 00:50:52 +0000408 line = self.file.readline()
409 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000410 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000411 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000412 if strip_crlf:
413 if line[-2:] == _CRLF:
414 line = line[:-2]
415 elif line[-1:] in _CRLF:
416 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000417 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000418
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000419 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000420 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000421 Raise various errors if the response indicates an error.
422 Returns an unicode string."""
423 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000424 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000425 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000426 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000427 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000428 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000429 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000430 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000431 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000432 raise NNTPProtocolError(resp)
433 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000434
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000435 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000436 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000437 Raise various errors if the response indicates an error.
438
439 Returns a (response, lines) tuple where `response` is an unicode
440 string and `lines` is a list of bytes objects.
441 If `file` is a file-like object, it must be open in binary mode.
442 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000443
444 openedFile = None
445 try:
446 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000447 if isinstance(file, (str, bytes)):
448 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000449
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000450 resp = self._getresp()
451 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000452 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000453
454 lines = []
455 if file is not None:
456 # XXX lines = None instead?
457 terminators = (b'.' + _CRLF, b'.\n')
458 while 1:
459 line = self._getline(False)
460 if line in terminators:
461 break
462 if line.startswith(b'..'):
463 line = line[1:]
464 file.write(line)
465 else:
466 terminator = b'.'
467 while 1:
468 line = self._getline()
469 if line == terminator:
470 break
471 if line.startswith(b'..'):
472 line = line[1:]
473 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000474 finally:
475 # If this method created the file, then it must close it
476 if openedFile:
477 openedFile.close()
478
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000479 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000480
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000481 def _shortcmd(self, line):
482 """Internal: send a command and get the response.
483 Same return value as _getresp()."""
484 self._putcmd(line)
485 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000486
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000487 def _longcmd(self, line, file=None):
488 """Internal: send a command and get the response plus following text.
489 Same return value as _getlongresp()."""
490 self._putcmd(line)
491 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000492
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000493 def _longcmdstring(self, line, file=None):
494 """Internal: send a command and get the response plus following text.
495 Same as _longcmd() and _getlongresp(), except that the returned `lines`
496 are unicode strings rather than bytes objects.
497 """
498 self._putcmd(line)
499 resp, list = self._getlongresp(file)
500 return resp, [line.decode(self.encoding, self.errors)
501 for line in list]
502
503 def _getoverviewfmt(self):
504 """Internal: get the overview format. Queries the server if not
505 already done, else returns the cached value."""
506 try:
507 return self._cachedoverviewfmt
508 except AttributeError:
509 pass
510 try:
511 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
512 except NNTPPermanentError:
513 # Not supported by server?
514 fmt = _DEFAULT_OVERVIEW_FMT[:]
515 else:
516 fmt = _parse_overview_fmt(lines)
517 self._cachedoverviewfmt = fmt
518 return fmt
519
520 def _grouplist(self, lines):
521 # Parse lines into "group last first flag"
522 return [GroupInfo(*line.split()) for line in lines]
523
524 def capabilities(self):
525 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000526 Return:
527 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000528 - caps: a dictionary mapping capability names to lists of tokens
529 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
530 """
531 caps = {}
532 resp, lines = self._longcmdstring("CAPABILITIES")
533 for line in lines:
534 name, *tokens = line.split()
535 caps[name] = tokens
536 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000537
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000538 def newgroups(self, date, *, file=None):
539 """Process a NEWGROUPS command. Arguments:
540 - date: a date or datetime object
541 Return:
542 - resp: server response if successful
543 - list: list of newsgroup names
544 """
545 if not isinstance(date, (datetime.date, datetime.date)):
546 raise TypeError(
547 "the date parameter must be a date or datetime object, "
548 "not '{:40}'".format(date.__class__.__name__))
549 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
550 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
551 resp, lines = self._longcmdstring(cmd, file)
552 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000553
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000554 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000555 """Process a NEWNEWS command. Arguments:
556 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000557 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000558 Return:
559 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000560 - list: list of message ids
561 """
562 if not isinstance(date, (datetime.date, datetime.date)):
563 raise TypeError(
564 "the date parameter must be a date or datetime object, "
565 "not '{:40}'".format(date.__class__.__name__))
566 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
567 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
568 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000569
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000570 def list(self, *, file=None):
571 """Process a LIST command. Argument:
572 - file: Filename string or file object to store the result in
573 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000574 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000575 - list: list of (group, last, first, flag) (strings)
576 """
577 resp, lines = self._longcmdstring('LIST', file)
578 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000579
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000580 def _getdescriptions(self, group_pattern, return_all):
581 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
582 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
583 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
584 if not resp.startswith('215'):
585 # Now the deprecated XGTITLE. This either raises an error
586 # or succeeds with the same output structure as LIST
587 # NEWSGROUPS.
588 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
589 groups = {}
590 for raw_line in lines:
591 match = line_pat.search(raw_line.strip())
592 if match:
593 name, desc = match.group(1, 2)
594 if not return_all:
595 return desc
596 groups[name] = desc
597 if return_all:
598 return resp, groups
599 else:
600 # Nothing found
601 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000602
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000603 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000604 """Get a description for a single group. If more than one
605 group matches ('group' is a pattern), return the first. If no
606 group matches, return an empty string.
607
608 This elides the response code from the server, since it can
609 only be '215' or '285' (for xgtitle) anyway. If the response
610 code is needed, use the 'descriptions' method.
611
612 NOTE: This neither checks for a wildcard in 'group' nor does
613 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000614 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000615
616 def descriptions(self, group_pattern):
617 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000618 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000619
Tim Peters2344fae2001-01-15 00:50:52 +0000620 def group(self, name):
621 """Process a GROUP command. Argument:
622 - group: the group name
623 Returns:
624 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000625 - count: number of articles
626 - first: first article number
627 - last: last article number
628 - name: the group name
629 """
630 resp = self._shortcmd('GROUP ' + name)
631 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000632 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000633 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000634 count = first = last = 0
635 n = len(words)
636 if n > 1:
637 count = words[1]
638 if n > 2:
639 first = words[2]
640 if n > 3:
641 last = words[3]
642 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000643 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000644 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000645
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000646 def help(self, *, file=None):
647 """Process a HELP command. Argument:
648 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000649 Returns:
650 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000651 - list: list of strings returned by the server in response to the
652 HELP command
653 """
654 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000655
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000656 def _statparse(self, resp):
657 """Internal: parse the response line of a STAT, NEXT, LAST,
658 ARTICLE, HEAD or BODY command."""
659 if not resp.startswith('22'):
660 raise NNTPReplyError(resp)
661 words = resp.split()
662 art_num = int(words[1])
663 message_id = words[2]
664 return resp, art_num, message_id
665
666 def _statcmd(self, line):
667 """Internal: process a STAT, NEXT or LAST command."""
668 resp = self._shortcmd(line)
669 return self._statparse(resp)
670
671 def stat(self, message_spec=None):
672 """Process a STAT command. Argument:
673 - message_spec: article number or message id (if not specified,
674 the current article is selected)
675 Returns:
676 - resp: server response if successful
677 - art_num: the article number
678 - message_id: the message id
679 """
680 if message_spec:
681 return self._statcmd('STAT {0}'.format(message_spec))
682 else:
683 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000684
Tim Peters2344fae2001-01-15 00:50:52 +0000685 def next(self):
686 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000687 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000688
Tim Peters2344fae2001-01-15 00:50:52 +0000689 def last(self):
690 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000691 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000692
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000693 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000694 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000695 resp, lines = self._longcmd(line, file)
696 resp, art_num, message_id = self._statparse(resp)
697 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000698
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000699 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000700 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000701 - message_spec: article number or message id
702 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000703 Returns:
704 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000705 - ArticleInfo: (article number, message id, list of header lines)
706 """
707 if message_spec is not None:
708 cmd = 'HEAD {0}'.format(message_spec)
709 else:
710 cmd = 'HEAD'
711 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000712
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000713 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000714 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000715 - message_spec: article number or message id
716 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000717 Returns:
718 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000719 - ArticleInfo: (article number, message id, list of body lines)
720 """
721 if message_spec is not None:
722 cmd = 'BODY {0}'.format(message_spec)
723 else:
724 cmd = 'BODY'
725 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000726
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000727 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000728 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000729 - message_spec: article number or message id
730 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000731 Returns:
732 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000733 - ArticleInfo: (article number, message id, list of article lines)
734 """
735 if message_spec is not None:
736 cmd = 'ARTICLE {0}'.format(message_spec)
737 else:
738 cmd = 'ARTICLE'
739 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000740
Tim Peters2344fae2001-01-15 00:50:52 +0000741 def slave(self):
742 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000743 - resp: server response if successful
744 """
745 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000746
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000747 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000748 """Process an XHDR command (optional server extension). Arguments:
749 - hdr: the header type (e.g. 'subject')
750 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000751 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000752 Returns:
753 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000754 - list: list of (nr, value) strings
755 """
756 pat = re.compile('^([0-9]+) ?(.*)\n?')
757 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
758 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000759 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000760 return m.group(1, 2) if m else line
761 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000762
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000763 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000764 """Process an XOVER command (optional server extension) Arguments:
765 - start: start of range
766 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000767 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000768 Returns:
769 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000770 - list: list of dicts containing the response fields
771 """
772 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
773 file)
774 fmt = self._getoverviewfmt()
775 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000776
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000777 def over(self, message_spec, *, file=None):
778 """Process an OVER command. If the command isn't supported, fall
779 back to XOVER. Arguments:
780 - message_spec:
781 - either a message id, indicating the article to fetch
782 information about
783 - or a (start, end) tuple, indicating a range of article numbers;
784 if end is None, information up to the newest message will be
785 retrieved
786 - or None, indicating the current article number must be used
787 - file: Filename string or file object to store the result in
788 Returns:
789 - resp: server response if successful
790 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000791
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000792 NOTE: the "message id" form isn't supported by XOVER
793 """
794 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
795 if isinstance(message_spec, (tuple, list)):
796 start, end = message_spec
797 cmd += ' {0}-{1}'.format(start, end or '')
798 elif message_spec is not None:
799 cmd = cmd + ' ' + message_spec
800 resp, lines = self._longcmdstring(cmd, file)
801 fmt = self._getoverviewfmt()
802 return resp, _parse_overview(lines, fmt)
803
804 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000805 """Process an XGTITLE command (optional server extension) Arguments:
806 - group: group name wildcard (i.e. news.*)
807 Returns:
808 - resp: server response if successful
809 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000810 warnings.warn("The XGTITLE extension is not actively used, "
811 "use descriptions() instead",
812 PendingDeprecationWarning, 2)
813 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
814 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000815 lines = []
816 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000817 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000818 if match:
819 lines.append(match.group(1, 2))
820 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000821
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000822 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000823 """Process an XPATH command (optional server extension) Arguments:
824 - id: Message id of article
825 Returns:
826 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000827 path: directory path to article
828 """
829 warnings.warn("The XPATH extension is not actively used",
830 PendingDeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000831
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000832 resp = self._shortcmd('XPATH {0}'.format(id))
833 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000834 raise NNTPReplyError(resp)
835 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000836 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000837 except ValueError:
838 raise NNTPReplyError(resp)
839 else:
840 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000841
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000842 def date(self):
843 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000844 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000845 - resp: server response if successful
846 - date: datetime object
847 """
848 resp = self._shortcmd("DATE")
849 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000850 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000851 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000852 if len(elem) != 2:
853 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000854 date = elem[1]
855 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000856 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000857 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000858
Christian Heimes933238a2008-11-05 19:44:21 +0000859 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000860 resp = self._shortcmd(command)
861 # Raises a specific exception if posting is not allowed
862 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000863 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000864 if isinstance(f, (bytes, bytearray)):
865 f = f.splitlines()
866 # We don't use _putline() because:
867 # - we don't want additional CRLF if the file or iterable is already
868 # in the right format
869 # - we don't want a spurious flush() after each line is written
870 for line in f:
871 if not line.endswith(_CRLF):
872 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000873 if line.startswith(b'.'):
874 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000875 self.file.write(line)
876 self.file.write(b".\r\n")
877 self.file.flush()
878 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000879
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000880 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000881 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000882 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000883 Returns:
884 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000885 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000886
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000887 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000888 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000889 - message_id: message-id of the article
890 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000891 Returns:
892 - resp: server response if successful
893 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000894 return self._post('IHAVE {0}'.format(message_id), data)
895
896 def _close(self):
897 self.file.close()
898 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000899
Tim Peters2344fae2001-01-15 00:50:52 +0000900 def quit(self):
901 """Process a QUIT command and close the socket. Returns:
902 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000903 try:
904 resp = self._shortcmd('QUIT')
905 finally:
906 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000907 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000908
909
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000910class NNTP(_NNTPBase):
911
912 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
913 readermode=None, usenetrc=True,
914 timeout=_GLOBAL_DEFAULT_TIMEOUT):
915 """Initialize an instance. Arguments:
916 - host: hostname to connect to
917 - port: port to connect to (default the standard NNTP port)
918 - user: username to authenticate with
919 - password: password to use with username
920 - readermode: if true, send 'mode reader' command after
921 connecting.
922 - usenetrc: allow loading username and password from ~/.netrc file
923 if not specified explicitly
924 - timeout: timeout (in seconds) used for socket connections
925
926 readermode is sometimes necessary if you are connecting to an
927 NNTP server on the local machine and intend to call
928 reader-specific comamnds, such as `group'. If you get
929 unexpected NNTPPermanentErrors, you might need to set
930 readermode.
931 """
932 self.host = host
933 self.port = port
934 self.sock = socket.create_connection((host, port), timeout)
935 file = self.sock.makefile("rwb")
936 _NNTPBase.__init__(self, file, user, password,
937 readermode, usenetrc, timeout)
938
939 def _close(self):
940 try:
941 _NNTPBase._close(self)
942 finally:
943 self.sock.close()
944
945
Neal Norwitzef679562002-11-14 02:19:44 +0000946# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000947if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000948 import argparse
949 from email.utils import parsedate
950
951 parser = argparse.ArgumentParser(description="""\
952 nntplib built-in demo - display the latest articles in a newsgroup""")
953 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
954 help='group to fetch messages from (default: %(default)s)')
955 parser.add_argument('-s', '--server', default='news.gmane.org',
956 help='NNTP server hostname (default: %(default)s)')
957 parser.add_argument('-p', '--port', default=NNTP_PORT, type=int,
958 help='NNTP port number (default: %(default)s)')
959 parser.add_argument('-n', '--nb-articles', default=10, type=int,
960 help='number of articles to fetch (default: %(default)s)')
961 args = parser.parse_args()
962
963 s = NNTP(host=args.server, port=args.port)
964 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000965 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000966
967 def cut(s, lim):
968 if len(s) > lim:
969 s = s[:lim - 4] + "..."
970 return s
971
972 first = str(int(last) - args.nb_articles + 1)
973 resp, overviews = s.xover(first, last)
974 for artnum, over in overviews:
975 author = decode_header(over['from']).split('<', 1)[0]
976 subject = decode_header(over['subject'])
977 lines = int(over[':lines'])
978 print("{:7} {:20} {:42} ({})".format(
979 artnum, cut(author, 20), cut(subject, 42), lines)
980 )
981
982 s.quit()