blob: 409342c78cd910a17aeb6decb2468a877d08d202 [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
Antoine Pitroua5785b12010-09-29 16:19:50 +0000282 def __init__(self, file, host, user=None, password=None,
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000283 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)
Antoine Pitroua5785b12010-09-29 16:19:50 +0000287 - host: hostname of the server (used if `usenetrc` is True)
Tim Peters2344fae2001-01-15 00:50:52 +0000288 - user: username to authenticate with
289 - password: password to use with username
290 - readermode: if true, send 'mode reader' command after
291 connecting.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000292 - usenetrc: allow loading username and password from ~/.netrc file
293 if not specified explicitly
294 - timeout: timeout (in seconds) used for socket connections
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000295
Tim Peters2344fae2001-01-15 00:50:52 +0000296 readermode is sometimes necessary if you are connecting to an
297 NNTP server on the local machine and intend to call
298 reader-specific comamnds, such as `group'. If you get
299 unexpected NNTPPermanentErrors, you might need to set
300 readermode.
301 """
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000302 self.file = file
Tim Peters2344fae2001-01-15 00:50:52 +0000303 self.debugging = 0
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000304 self.welcome = self._getresp()
Tim Petersdfb673b2001-01-16 07:12:46 +0000305
Thomas Wouters47adcba2001-01-16 06:35:14 +0000306 # 'mode reader' is sometimes necessary to enable 'reader' mode.
Tim Petersdfb673b2001-01-16 07:12:46 +0000307 # However, the order in which 'mode reader' and 'authinfo' need to
Thomas Wouters47adcba2001-01-16 06:35:14 +0000308 # arrive differs between some NNTP servers. Try to send
309 # 'mode reader', and if it fails with an authorization failed
310 # error, try again after sending authinfo.
311 readermode_afterauth = 0
Tim Peters2344fae2001-01-15 00:50:52 +0000312 if readermode:
313 try:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000314 self.welcome = self._shortcmd('mode reader')
Tim Peters2344fae2001-01-15 00:50:52 +0000315 except NNTPPermanentError:
316 # error 500, probably 'not implemented'
317 pass
Guido van Rossumb940e112007-01-10 16:19:56 +0000318 except NNTPTemporaryError as e:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000319 if user and e.response.startswith('480'):
Thomas Wouters47adcba2001-01-16 06:35:14 +0000320 # Need authorization before 'mode reader'
321 readermode_afterauth = 1
322 else:
323 raise
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000324 # If no login/password was specified, try to get them from ~/.netrc
325 # Presume that if .netc has an entry, NNRP authentication is required.
Eric S. Raymond782d9402002-11-17 17:53:12 +0000326 try:
Martin v. Löwis9513e342004-08-03 14:36:32 +0000327 if usenetrc and not user:
Eric S. Raymond782d9402002-11-17 17:53:12 +0000328 import netrc
329 credentials = netrc.netrc()
330 auth = credentials.authenticators(host)
331 if auth:
332 user = auth[0]
333 password = auth[2]
334 except IOError:
335 pass
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000336 # Perform NNTP authentication if needed.
Tim Peters2344fae2001-01-15 00:50:52 +0000337 if user:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000338 resp = self._shortcmd('authinfo user '+user)
339 if resp.startswith('381'):
Tim Peters2344fae2001-01-15 00:50:52 +0000340 if not password:
341 raise NNTPReplyError(resp)
342 else:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000343 resp = self._shortcmd(
Tim Peters2344fae2001-01-15 00:50:52 +0000344 'authinfo pass '+password)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000345 if not resp.startswith('281'):
Tim Peters2344fae2001-01-15 00:50:52 +0000346 raise NNTPPermanentError(resp)
Thomas Wouters47adcba2001-01-16 06:35:14 +0000347 if readermode_afterauth:
348 try:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000349 self.welcome = self._shortcmd('mode reader')
Thomas Wouters47adcba2001-01-16 06:35:14 +0000350 except NNTPPermanentError:
351 # error 500, probably 'not implemented'
352 pass
Tim Petersdfb673b2001-01-16 07:12:46 +0000353
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000354 # Inquire about capabilities (RFC 3977)
355 self.nntp_version = 1
356 try:
357 resp, caps = self.capabilities()
358 except NNTPPermanentError:
359 # Server doesn't support capabilities
360 self._caps = {}
361 else:
362 self._caps = caps
363 if 'VERSION' in caps:
364 self.nntp_version = int(caps['VERSION'][0])
Guido van Rossumc629d341992-11-05 10:43:02 +0000365
Tim Peters2344fae2001-01-15 00:50:52 +0000366 def getwelcome(self):
367 """Get the welcome message from the server
368 (this is read and squirreled away by __init__()).
369 If the response code is 200, posting is allowed;
370 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000371
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000372 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000373 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000374
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000375 def getcapabilities(self):
376 """Get the server capabilities, as read by __init__().
377 If the CAPABILITIES command is not supported, an empty dict is
378 returned."""
379 return self._caps
380
Tim Peters2344fae2001-01-15 00:50:52 +0000381 def set_debuglevel(self, level):
382 """Set the debugging level. Argument 'level' means:
383 0: no debugging output (default)
384 1: print commands and responses but not body text etc.
385 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000386
Tim Peters2344fae2001-01-15 00:50:52 +0000387 self.debugging = level
388 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000389
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000390 def _putline(self, line):
391 """Internal: send one line to the server, appending CRLF.
392 The `line` must be a bytes-like object."""
393 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000394 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000395 self.file.write(line)
396 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000397
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000398 def _putcmd(self, line):
399 """Internal: send one command to the server (through _putline()).
400 The `line` must be an unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000401 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000402 line = line.encode(self.encoding, self.errors)
403 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000404
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000405 def _getline(self, strip_crlf=True):
406 """Internal: return one line from the server, stripping _CRLF.
407 Raise EOFError if the connection is closed.
408 Returns a bytes object."""
Tim Peters2344fae2001-01-15 00:50:52 +0000409 line = self.file.readline()
410 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000411 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000412 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000413 if strip_crlf:
414 if line[-2:] == _CRLF:
415 line = line[:-2]
416 elif line[-1:] in _CRLF:
417 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000418 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000419
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000420 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000421 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000422 Raise various errors if the response indicates an error.
423 Returns an unicode string."""
424 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000425 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000426 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000427 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000428 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000429 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000430 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000431 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000432 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000433 raise NNTPProtocolError(resp)
434 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000435
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000436 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000437 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000438 Raise various errors if the response indicates an error.
439
440 Returns a (response, lines) tuple where `response` is an unicode
441 string and `lines` is a list of bytes objects.
442 If `file` is a file-like object, it must be open in binary mode.
443 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000444
445 openedFile = None
446 try:
447 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000448 if isinstance(file, (str, bytes)):
449 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000450
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000451 resp = self._getresp()
452 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000453 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000454
455 lines = []
456 if file is not None:
457 # XXX lines = None instead?
458 terminators = (b'.' + _CRLF, b'.\n')
459 while 1:
460 line = self._getline(False)
461 if line in terminators:
462 break
463 if line.startswith(b'..'):
464 line = line[1:]
465 file.write(line)
466 else:
467 terminator = b'.'
468 while 1:
469 line = self._getline()
470 if line == terminator:
471 break
472 if line.startswith(b'..'):
473 line = line[1:]
474 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000475 finally:
476 # If this method created the file, then it must close it
477 if openedFile:
478 openedFile.close()
479
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000480 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000481
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000482 def _shortcmd(self, line):
483 """Internal: send a command and get the response.
484 Same return value as _getresp()."""
485 self._putcmd(line)
486 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000487
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000488 def _longcmd(self, line, file=None):
489 """Internal: send a command and get the response plus following text.
490 Same return value as _getlongresp()."""
491 self._putcmd(line)
492 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000493
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000494 def _longcmdstring(self, line, file=None):
495 """Internal: send a command and get the response plus following text.
496 Same as _longcmd() and _getlongresp(), except that the returned `lines`
497 are unicode strings rather than bytes objects.
498 """
499 self._putcmd(line)
500 resp, list = self._getlongresp(file)
501 return resp, [line.decode(self.encoding, self.errors)
502 for line in list]
503
504 def _getoverviewfmt(self):
505 """Internal: get the overview format. Queries the server if not
506 already done, else returns the cached value."""
507 try:
508 return self._cachedoverviewfmt
509 except AttributeError:
510 pass
511 try:
512 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
513 except NNTPPermanentError:
514 # Not supported by server?
515 fmt = _DEFAULT_OVERVIEW_FMT[:]
516 else:
517 fmt = _parse_overview_fmt(lines)
518 self._cachedoverviewfmt = fmt
519 return fmt
520
521 def _grouplist(self, lines):
522 # Parse lines into "group last first flag"
523 return [GroupInfo(*line.split()) for line in lines]
524
525 def capabilities(self):
526 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000527 Return:
528 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000529 - caps: a dictionary mapping capability names to lists of tokens
530 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
531 """
532 caps = {}
533 resp, lines = self._longcmdstring("CAPABILITIES")
534 for line in lines:
535 name, *tokens = line.split()
536 caps[name] = tokens
537 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000538
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000539 def newgroups(self, date, *, file=None):
540 """Process a NEWGROUPS command. Arguments:
541 - date: a date or datetime object
542 Return:
543 - resp: server response if successful
544 - list: list of newsgroup names
545 """
546 if not isinstance(date, (datetime.date, datetime.date)):
547 raise TypeError(
548 "the date parameter must be a date or datetime object, "
549 "not '{:40}'".format(date.__class__.__name__))
550 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
551 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
552 resp, lines = self._longcmdstring(cmd, file)
553 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000554
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000555 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000556 """Process a NEWNEWS command. Arguments:
557 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000558 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000559 Return:
560 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000561 - list: list of message ids
562 """
563 if not isinstance(date, (datetime.date, datetime.date)):
564 raise TypeError(
565 "the date parameter must be a date or datetime object, "
566 "not '{:40}'".format(date.__class__.__name__))
567 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
568 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
569 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000570
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000571 def list(self, *, file=None):
572 """Process a LIST command. Argument:
573 - file: Filename string or file object to store the result in
574 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000575 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000576 - list: list of (group, last, first, flag) (strings)
577 """
578 resp, lines = self._longcmdstring('LIST', file)
579 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000580
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000581 def _getdescriptions(self, group_pattern, return_all):
582 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
583 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
584 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
585 if not resp.startswith('215'):
586 # Now the deprecated XGTITLE. This either raises an error
587 # or succeeds with the same output structure as LIST
588 # NEWSGROUPS.
589 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
590 groups = {}
591 for raw_line in lines:
592 match = line_pat.search(raw_line.strip())
593 if match:
594 name, desc = match.group(1, 2)
595 if not return_all:
596 return desc
597 groups[name] = desc
598 if return_all:
599 return resp, groups
600 else:
601 # Nothing found
602 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000603
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000604 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000605 """Get a description for a single group. If more than one
606 group matches ('group' is a pattern), return the first. If no
607 group matches, return an empty string.
608
609 This elides the response code from the server, since it can
610 only be '215' or '285' (for xgtitle) anyway. If the response
611 code is needed, use the 'descriptions' method.
612
613 NOTE: This neither checks for a wildcard in 'group' nor does
614 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000615 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000616
617 def descriptions(self, group_pattern):
618 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000619 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000620
Tim Peters2344fae2001-01-15 00:50:52 +0000621 def group(self, name):
622 """Process a GROUP command. Argument:
623 - group: the group name
624 Returns:
625 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000626 - count: number of articles
627 - first: first article number
628 - last: last article number
629 - name: the group name
630 """
631 resp = self._shortcmd('GROUP ' + name)
632 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000633 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000634 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000635 count = first = last = 0
636 n = len(words)
637 if n > 1:
638 count = words[1]
639 if n > 2:
640 first = words[2]
641 if n > 3:
642 last = words[3]
643 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000644 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000645 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000646
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000647 def help(self, *, file=None):
648 """Process a HELP command. Argument:
649 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000650 Returns:
651 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000652 - list: list of strings returned by the server in response to the
653 HELP command
654 """
655 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000656
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000657 def _statparse(self, resp):
658 """Internal: parse the response line of a STAT, NEXT, LAST,
659 ARTICLE, HEAD or BODY command."""
660 if not resp.startswith('22'):
661 raise NNTPReplyError(resp)
662 words = resp.split()
663 art_num = int(words[1])
664 message_id = words[2]
665 return resp, art_num, message_id
666
667 def _statcmd(self, line):
668 """Internal: process a STAT, NEXT or LAST command."""
669 resp = self._shortcmd(line)
670 return self._statparse(resp)
671
672 def stat(self, message_spec=None):
673 """Process a STAT command. Argument:
674 - message_spec: article number or message id (if not specified,
675 the current article is selected)
676 Returns:
677 - resp: server response if successful
678 - art_num: the article number
679 - message_id: the message id
680 """
681 if message_spec:
682 return self._statcmd('STAT {0}'.format(message_spec))
683 else:
684 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000685
Tim Peters2344fae2001-01-15 00:50:52 +0000686 def next(self):
687 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000688 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000689
Tim Peters2344fae2001-01-15 00:50:52 +0000690 def last(self):
691 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000692 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000693
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000694 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000695 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000696 resp, lines = self._longcmd(line, file)
697 resp, art_num, message_id = self._statparse(resp)
698 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000699
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000700 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000701 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000702 - message_spec: article number or message id
703 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000704 Returns:
705 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000706 - ArticleInfo: (article number, message id, list of header lines)
707 """
708 if message_spec is not None:
709 cmd = 'HEAD {0}'.format(message_spec)
710 else:
711 cmd = 'HEAD'
712 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000713
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000714 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000715 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000716 - message_spec: article number or message id
717 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000718 Returns:
719 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000720 - ArticleInfo: (article number, message id, list of body lines)
721 """
722 if message_spec is not None:
723 cmd = 'BODY {0}'.format(message_spec)
724 else:
725 cmd = 'BODY'
726 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000727
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000728 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000729 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000730 - message_spec: article number or message id
731 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000732 Returns:
733 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000734 - ArticleInfo: (article number, message id, list of article lines)
735 """
736 if message_spec is not None:
737 cmd = 'ARTICLE {0}'.format(message_spec)
738 else:
739 cmd = 'ARTICLE'
740 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000741
Tim Peters2344fae2001-01-15 00:50:52 +0000742 def slave(self):
743 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000744 - resp: server response if successful
745 """
746 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000747
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000748 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000749 """Process an XHDR command (optional server extension). Arguments:
750 - hdr: the header type (e.g. 'subject')
751 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000752 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000753 Returns:
754 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000755 - list: list of (nr, value) strings
756 """
757 pat = re.compile('^([0-9]+) ?(.*)\n?')
758 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
759 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000760 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000761 return m.group(1, 2) if m else line
762 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000763
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000764 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000765 """Process an XOVER command (optional server extension) Arguments:
766 - start: start of range
767 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000768 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000769 Returns:
770 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000771 - list: list of dicts containing the response fields
772 """
773 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
774 file)
775 fmt = self._getoverviewfmt()
776 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000777
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000778 def over(self, message_spec, *, file=None):
779 """Process an OVER command. If the command isn't supported, fall
780 back to XOVER. Arguments:
781 - message_spec:
782 - either a message id, indicating the article to fetch
783 information about
784 - or a (start, end) tuple, indicating a range of article numbers;
785 if end is None, information up to the newest message will be
786 retrieved
787 - or None, indicating the current article number must be used
788 - file: Filename string or file object to store the result in
789 Returns:
790 - resp: server response if successful
791 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000792
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000793 NOTE: the "message id" form isn't supported by XOVER
794 """
795 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
796 if isinstance(message_spec, (tuple, list)):
797 start, end = message_spec
798 cmd += ' {0}-{1}'.format(start, end or '')
799 elif message_spec is not None:
800 cmd = cmd + ' ' + message_spec
801 resp, lines = self._longcmdstring(cmd, file)
802 fmt = self._getoverviewfmt()
803 return resp, _parse_overview(lines, fmt)
804
805 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000806 """Process an XGTITLE command (optional server extension) Arguments:
807 - group: group name wildcard (i.e. news.*)
808 Returns:
809 - resp: server response if successful
810 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000811 warnings.warn("The XGTITLE extension is not actively used, "
812 "use descriptions() instead",
813 PendingDeprecationWarning, 2)
814 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
815 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000816 lines = []
817 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000818 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000819 if match:
820 lines.append(match.group(1, 2))
821 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000822
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000823 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000824 """Process an XPATH command (optional server extension) Arguments:
825 - id: Message id of article
826 Returns:
827 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000828 path: directory path to article
829 """
830 warnings.warn("The XPATH extension is not actively used",
831 PendingDeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000832
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000833 resp = self._shortcmd('XPATH {0}'.format(id))
834 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000835 raise NNTPReplyError(resp)
836 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000837 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000838 except ValueError:
839 raise NNTPReplyError(resp)
840 else:
841 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000842
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000843 def date(self):
844 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000845 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000846 - resp: server response if successful
847 - date: datetime object
848 """
849 resp = self._shortcmd("DATE")
850 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000851 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000852 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000853 if len(elem) != 2:
854 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000855 date = elem[1]
856 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000857 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000858 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000859
Christian Heimes933238a2008-11-05 19:44:21 +0000860 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000861 resp = self._shortcmd(command)
862 # Raises a specific exception if posting is not allowed
863 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000864 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000865 if isinstance(f, (bytes, bytearray)):
866 f = f.splitlines()
867 # We don't use _putline() because:
868 # - we don't want additional CRLF if the file or iterable is already
869 # in the right format
870 # - we don't want a spurious flush() after each line is written
871 for line in f:
872 if not line.endswith(_CRLF):
873 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000874 if line.startswith(b'.'):
875 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000876 self.file.write(line)
877 self.file.write(b".\r\n")
878 self.file.flush()
879 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000880
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000881 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000882 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000883 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000884 Returns:
885 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000886 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000887
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000888 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000889 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000890 - message_id: message-id of the article
891 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000892 Returns:
893 - resp: server response if successful
894 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000895 return self._post('IHAVE {0}'.format(message_id), data)
896
897 def _close(self):
898 self.file.close()
899 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000900
Tim Peters2344fae2001-01-15 00:50:52 +0000901 def quit(self):
902 """Process a QUIT command and close the socket. Returns:
903 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000904 try:
905 resp = self._shortcmd('QUIT')
906 finally:
907 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000908 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000909
910
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000911class NNTP(_NNTPBase):
912
913 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
914 readermode=None, usenetrc=True,
915 timeout=_GLOBAL_DEFAULT_TIMEOUT):
916 """Initialize an instance. Arguments:
917 - host: hostname to connect to
918 - port: port to connect to (default the standard NNTP port)
919 - user: username to authenticate with
920 - password: password to use with username
921 - readermode: if true, send 'mode reader' command after
922 connecting.
923 - usenetrc: allow loading username and password from ~/.netrc file
924 if not specified explicitly
925 - timeout: timeout (in seconds) used for socket connections
926
927 readermode is sometimes necessary if you are connecting to an
928 NNTP server on the local machine and intend to call
929 reader-specific comamnds, such as `group'. If you get
930 unexpected NNTPPermanentErrors, you might need to set
931 readermode.
932 """
933 self.host = host
934 self.port = port
935 self.sock = socket.create_connection((host, port), timeout)
936 file = self.sock.makefile("rwb")
Antoine Pitroua5785b12010-09-29 16:19:50 +0000937 _NNTPBase.__init__(self, file, host, user, password,
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000938 readermode, usenetrc, timeout)
939
940 def _close(self):
941 try:
942 _NNTPBase._close(self)
943 finally:
944 self.sock.close()
945
946
Neal Norwitzef679562002-11-14 02:19:44 +0000947# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000948if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000949 import argparse
950 from email.utils import parsedate
951
952 parser = argparse.ArgumentParser(description="""\
953 nntplib built-in demo - display the latest articles in a newsgroup""")
954 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
955 help='group to fetch messages from (default: %(default)s)')
956 parser.add_argument('-s', '--server', default='news.gmane.org',
957 help='NNTP server hostname (default: %(default)s)')
958 parser.add_argument('-p', '--port', default=NNTP_PORT, type=int,
959 help='NNTP port number (default: %(default)s)')
960 parser.add_argument('-n', '--nb-articles', default=10, type=int,
961 help='number of articles to fetch (default: %(default)s)')
962 args = parser.parse_args()
963
964 s = NNTP(host=args.server, port=args.port)
965 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000966 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000967
968 def cut(s, lim):
969 if len(s) > lim:
970 s = s[:lim - 4] + "..."
971 return s
972
973 first = str(int(last) - args.nb_articles + 1)
974 resp, overviews = s.xover(first, last)
975 for artnum, over in overviews:
976 author = decode_header(over['from']).split('<', 1)[0]
977 subject = decode_header(over['subject'])
978 lines = int(over[':lines'])
979 print("{:7} {:20} {:42} ({})".format(
980 artnum, cut(author, 20), cut(subject, 42), lines)
981 )
982
983 s.quit()