blob: b067d6b3dfc45f909eaf3c692a500506bcbc665b [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:
Antoine Pitrouf80b3f72010-11-02 22:31:52 +0000364 # The server can advertise several supported versions,
365 # choose the highest.
366 self.nntp_version = max(map(int, caps['VERSION']))
Guido van Rossumc629d341992-11-05 10:43:02 +0000367
Tim Peters2344fae2001-01-15 00:50:52 +0000368 def getwelcome(self):
369 """Get the welcome message from the server
370 (this is read and squirreled away by __init__()).
371 If the response code is 200, posting is allowed;
372 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000373
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000374 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000375 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000376
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000377 def getcapabilities(self):
378 """Get the server capabilities, as read by __init__().
379 If the CAPABILITIES command is not supported, an empty dict is
380 returned."""
381 return self._caps
382
Tim Peters2344fae2001-01-15 00:50:52 +0000383 def set_debuglevel(self, level):
384 """Set the debugging level. Argument 'level' means:
385 0: no debugging output (default)
386 1: print commands and responses but not body text etc.
387 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000388
Tim Peters2344fae2001-01-15 00:50:52 +0000389 self.debugging = level
390 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000391
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000392 def _putline(self, line):
393 """Internal: send one line to the server, appending CRLF.
394 The `line` must be a bytes-like object."""
395 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000396 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000397 self.file.write(line)
398 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000399
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000400 def _putcmd(self, line):
401 """Internal: send one command to the server (through _putline()).
402 The `line` must be an unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000403 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000404 line = line.encode(self.encoding, self.errors)
405 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000406
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000407 def _getline(self, strip_crlf=True):
408 """Internal: return one line from the server, stripping _CRLF.
409 Raise EOFError if the connection is closed.
410 Returns a bytes object."""
Tim Peters2344fae2001-01-15 00:50:52 +0000411 line = self.file.readline()
412 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000413 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000414 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000415 if strip_crlf:
416 if line[-2:] == _CRLF:
417 line = line[:-2]
418 elif line[-1:] in _CRLF:
419 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000420 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000421
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000422 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000423 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000424 Raise various errors if the response indicates an error.
425 Returns an unicode string."""
426 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000427 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000428 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000429 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000430 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000431 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000432 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000433 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000434 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000435 raise NNTPProtocolError(resp)
436 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000437
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000438 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000439 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000440 Raise various errors if the response indicates an error.
441
442 Returns a (response, lines) tuple where `response` is an unicode
443 string and `lines` is a list of bytes objects.
444 If `file` is a file-like object, it must be open in binary mode.
445 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000446
447 openedFile = None
448 try:
449 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000450 if isinstance(file, (str, bytes)):
451 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000452
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000453 resp = self._getresp()
454 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000455 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000456
457 lines = []
458 if file is not None:
459 # XXX lines = None instead?
460 terminators = (b'.' + _CRLF, b'.\n')
461 while 1:
462 line = self._getline(False)
463 if line in terminators:
464 break
465 if line.startswith(b'..'):
466 line = line[1:]
467 file.write(line)
468 else:
469 terminator = b'.'
470 while 1:
471 line = self._getline()
472 if line == terminator:
473 break
474 if line.startswith(b'..'):
475 line = line[1:]
476 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000477 finally:
478 # If this method created the file, then it must close it
479 if openedFile:
480 openedFile.close()
481
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000482 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000483
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000484 def _shortcmd(self, line):
485 """Internal: send a command and get the response.
486 Same return value as _getresp()."""
487 self._putcmd(line)
488 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000489
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000490 def _longcmd(self, line, file=None):
491 """Internal: send a command and get the response plus following text.
492 Same return value as _getlongresp()."""
493 self._putcmd(line)
494 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000495
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000496 def _longcmdstring(self, line, file=None):
497 """Internal: send a command and get the response plus following text.
498 Same as _longcmd() and _getlongresp(), except that the returned `lines`
499 are unicode strings rather than bytes objects.
500 """
501 self._putcmd(line)
502 resp, list = self._getlongresp(file)
503 return resp, [line.decode(self.encoding, self.errors)
504 for line in list]
505
506 def _getoverviewfmt(self):
507 """Internal: get the overview format. Queries the server if not
508 already done, else returns the cached value."""
509 try:
510 return self._cachedoverviewfmt
511 except AttributeError:
512 pass
513 try:
514 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
515 except NNTPPermanentError:
516 # Not supported by server?
517 fmt = _DEFAULT_OVERVIEW_FMT[:]
518 else:
519 fmt = _parse_overview_fmt(lines)
520 self._cachedoverviewfmt = fmt
521 return fmt
522
523 def _grouplist(self, lines):
524 # Parse lines into "group last first flag"
525 return [GroupInfo(*line.split()) for line in lines]
526
527 def capabilities(self):
528 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000529 Return:
530 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000531 - caps: a dictionary mapping capability names to lists of tokens
532 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
533 """
534 caps = {}
535 resp, lines = self._longcmdstring("CAPABILITIES")
536 for line in lines:
537 name, *tokens = line.split()
538 caps[name] = tokens
539 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000540
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000541 def newgroups(self, date, *, file=None):
542 """Process a NEWGROUPS command. Arguments:
543 - date: a date or datetime object
544 Return:
545 - resp: server response if successful
546 - list: list of newsgroup names
547 """
548 if not isinstance(date, (datetime.date, datetime.date)):
549 raise TypeError(
550 "the date parameter must be a date or datetime object, "
551 "not '{:40}'".format(date.__class__.__name__))
552 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
553 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
554 resp, lines = self._longcmdstring(cmd, file)
555 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000556
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000557 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000558 """Process a NEWNEWS command. Arguments:
559 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000560 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000561 Return:
562 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000563 - list: list of message ids
564 """
565 if not isinstance(date, (datetime.date, datetime.date)):
566 raise TypeError(
567 "the date parameter must be a date or datetime object, "
568 "not '{:40}'".format(date.__class__.__name__))
569 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
570 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
571 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000572
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000573 def list(self, *, file=None):
574 """Process a LIST command. Argument:
575 - file: Filename string or file object to store the result in
576 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000577 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000578 - list: list of (group, last, first, flag) (strings)
579 """
580 resp, lines = self._longcmdstring('LIST', file)
581 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000582
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000583 def _getdescriptions(self, group_pattern, return_all):
584 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
585 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
586 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
587 if not resp.startswith('215'):
588 # Now the deprecated XGTITLE. This either raises an error
589 # or succeeds with the same output structure as LIST
590 # NEWSGROUPS.
591 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
592 groups = {}
593 for raw_line in lines:
594 match = line_pat.search(raw_line.strip())
595 if match:
596 name, desc = match.group(1, 2)
597 if not return_all:
598 return desc
599 groups[name] = desc
600 if return_all:
601 return resp, groups
602 else:
603 # Nothing found
604 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000605
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000606 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000607 """Get a description for a single group. If more than one
608 group matches ('group' is a pattern), return the first. If no
609 group matches, return an empty string.
610
611 This elides the response code from the server, since it can
612 only be '215' or '285' (for xgtitle) anyway. If the response
613 code is needed, use the 'descriptions' method.
614
615 NOTE: This neither checks for a wildcard in 'group' nor does
616 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000617 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000618
619 def descriptions(self, group_pattern):
620 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000621 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000622
Tim Peters2344fae2001-01-15 00:50:52 +0000623 def group(self, name):
624 """Process a GROUP command. Argument:
625 - group: the group name
626 Returns:
627 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000628 - count: number of articles
629 - first: first article number
630 - last: last article number
631 - name: the group name
632 """
633 resp = self._shortcmd('GROUP ' + name)
634 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000635 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000636 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000637 count = first = last = 0
638 n = len(words)
639 if n > 1:
640 count = words[1]
641 if n > 2:
642 first = words[2]
643 if n > 3:
644 last = words[3]
645 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000646 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000647 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000648
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000649 def help(self, *, file=None):
650 """Process a HELP command. Argument:
651 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000652 Returns:
653 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000654 - list: list of strings returned by the server in response to the
655 HELP command
656 """
657 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000658
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000659 def _statparse(self, resp):
660 """Internal: parse the response line of a STAT, NEXT, LAST,
661 ARTICLE, HEAD or BODY command."""
662 if not resp.startswith('22'):
663 raise NNTPReplyError(resp)
664 words = resp.split()
665 art_num = int(words[1])
666 message_id = words[2]
667 return resp, art_num, message_id
668
669 def _statcmd(self, line):
670 """Internal: process a STAT, NEXT or LAST command."""
671 resp = self._shortcmd(line)
672 return self._statparse(resp)
673
674 def stat(self, message_spec=None):
675 """Process a STAT command. Argument:
676 - message_spec: article number or message id (if not specified,
677 the current article is selected)
678 Returns:
679 - resp: server response if successful
680 - art_num: the article number
681 - message_id: the message id
682 """
683 if message_spec:
684 return self._statcmd('STAT {0}'.format(message_spec))
685 else:
686 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000687
Tim Peters2344fae2001-01-15 00:50:52 +0000688 def next(self):
689 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000690 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000691
Tim Peters2344fae2001-01-15 00:50:52 +0000692 def last(self):
693 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000694 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000695
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000696 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000697 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000698 resp, lines = self._longcmd(line, file)
699 resp, art_num, message_id = self._statparse(resp)
700 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000701
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000702 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000703 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000704 - message_spec: article number or message id
705 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000706 Returns:
707 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000708 - ArticleInfo: (article number, message id, list of header lines)
709 """
710 if message_spec is not None:
711 cmd = 'HEAD {0}'.format(message_spec)
712 else:
713 cmd = 'HEAD'
714 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000715
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000716 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000717 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000718 - message_spec: article number or message id
719 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000720 Returns:
721 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000722 - ArticleInfo: (article number, message id, list of body lines)
723 """
724 if message_spec is not None:
725 cmd = 'BODY {0}'.format(message_spec)
726 else:
727 cmd = 'BODY'
728 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000729
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000730 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000731 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000732 - message_spec: article number or message id
733 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000734 Returns:
735 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000736 - ArticleInfo: (article number, message id, list of article lines)
737 """
738 if message_spec is not None:
739 cmd = 'ARTICLE {0}'.format(message_spec)
740 else:
741 cmd = 'ARTICLE'
742 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000743
Tim Peters2344fae2001-01-15 00:50:52 +0000744 def slave(self):
745 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000746 - resp: server response if successful
747 """
748 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000749
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000750 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000751 """Process an XHDR command (optional server extension). Arguments:
752 - hdr: the header type (e.g. 'subject')
753 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000754 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000755 Returns:
756 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000757 - list: list of (nr, value) strings
758 """
759 pat = re.compile('^([0-9]+) ?(.*)\n?')
760 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
761 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000762 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000763 return m.group(1, 2) if m else line
764 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000765
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000766 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000767 """Process an XOVER command (optional server extension) Arguments:
768 - start: start of range
769 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000770 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000771 Returns:
772 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000773 - list: list of dicts containing the response fields
774 """
775 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
776 file)
777 fmt = self._getoverviewfmt()
778 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000779
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000780 def over(self, message_spec, *, file=None):
781 """Process an OVER command. If the command isn't supported, fall
782 back to XOVER. Arguments:
783 - message_spec:
784 - either a message id, indicating the article to fetch
785 information about
786 - or a (start, end) tuple, indicating a range of article numbers;
787 if end is None, information up to the newest message will be
788 retrieved
789 - or None, indicating the current article number must be used
790 - file: Filename string or file object to store the result in
791 Returns:
792 - resp: server response if successful
793 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000794
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000795 NOTE: the "message id" form isn't supported by XOVER
796 """
797 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
798 if isinstance(message_spec, (tuple, list)):
799 start, end = message_spec
800 cmd += ' {0}-{1}'.format(start, end or '')
801 elif message_spec is not None:
802 cmd = cmd + ' ' + message_spec
803 resp, lines = self._longcmdstring(cmd, file)
804 fmt = self._getoverviewfmt()
805 return resp, _parse_overview(lines, fmt)
806
807 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000808 """Process an XGTITLE command (optional server extension) Arguments:
809 - group: group name wildcard (i.e. news.*)
810 Returns:
811 - resp: server response if successful
812 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000813 warnings.warn("The XGTITLE extension is not actively used, "
814 "use descriptions() instead",
815 PendingDeprecationWarning, 2)
816 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
817 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000818 lines = []
819 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000820 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000821 if match:
822 lines.append(match.group(1, 2))
823 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000824
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000825 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000826 """Process an XPATH command (optional server extension) Arguments:
827 - id: Message id of article
828 Returns:
829 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000830 path: directory path to article
831 """
832 warnings.warn("The XPATH extension is not actively used",
833 PendingDeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000834
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000835 resp = self._shortcmd('XPATH {0}'.format(id))
836 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000837 raise NNTPReplyError(resp)
838 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000839 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000840 except ValueError:
841 raise NNTPReplyError(resp)
842 else:
843 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000844
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000845 def date(self):
846 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000847 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000848 - resp: server response if successful
849 - date: datetime object
850 """
851 resp = self._shortcmd("DATE")
852 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000853 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000854 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000855 if len(elem) != 2:
856 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000857 date = elem[1]
858 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000859 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000860 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000861
Christian Heimes933238a2008-11-05 19:44:21 +0000862 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000863 resp = self._shortcmd(command)
864 # Raises a specific exception if posting is not allowed
865 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000866 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000867 if isinstance(f, (bytes, bytearray)):
868 f = f.splitlines()
869 # We don't use _putline() because:
870 # - we don't want additional CRLF if the file or iterable is already
871 # in the right format
872 # - we don't want a spurious flush() after each line is written
873 for line in f:
874 if not line.endswith(_CRLF):
875 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000876 if line.startswith(b'.'):
877 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000878 self.file.write(line)
879 self.file.write(b".\r\n")
880 self.file.flush()
881 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000882
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000883 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000884 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000885 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000886 Returns:
887 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000888 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000889
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000890 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000891 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000892 - message_id: message-id of the article
893 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000894 Returns:
895 - resp: server response if successful
896 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000897 return self._post('IHAVE {0}'.format(message_id), data)
898
899 def _close(self):
900 self.file.close()
901 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000902
Tim Peters2344fae2001-01-15 00:50:52 +0000903 def quit(self):
904 """Process a QUIT command and close the socket. Returns:
905 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000906 try:
907 resp = self._shortcmd('QUIT')
908 finally:
909 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000910 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000911
912
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000913class NNTP(_NNTPBase):
914
915 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
916 readermode=None, usenetrc=True,
917 timeout=_GLOBAL_DEFAULT_TIMEOUT):
918 """Initialize an instance. Arguments:
919 - host: hostname to connect to
920 - port: port to connect to (default the standard NNTP port)
921 - user: username to authenticate with
922 - password: password to use with username
923 - readermode: if true, send 'mode reader' command after
924 connecting.
925 - usenetrc: allow loading username and password from ~/.netrc file
926 if not specified explicitly
927 - timeout: timeout (in seconds) used for socket connections
928
929 readermode is sometimes necessary if you are connecting to an
930 NNTP server on the local machine and intend to call
931 reader-specific comamnds, such as `group'. If you get
932 unexpected NNTPPermanentErrors, you might need to set
933 readermode.
934 """
935 self.host = host
936 self.port = port
937 self.sock = socket.create_connection((host, port), timeout)
938 file = self.sock.makefile("rwb")
Antoine Pitroua5785b12010-09-29 16:19:50 +0000939 _NNTPBase.__init__(self, file, host, user, password,
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000940 readermode, usenetrc, timeout)
941
942 def _close(self):
943 try:
944 _NNTPBase._close(self)
945 finally:
946 self.sock.close()
947
948
Neal Norwitzef679562002-11-14 02:19:44 +0000949# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000950if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000951 import argparse
952 from email.utils import parsedate
953
954 parser = argparse.ArgumentParser(description="""\
955 nntplib built-in demo - display the latest articles in a newsgroup""")
956 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
957 help='group to fetch messages from (default: %(default)s)')
958 parser.add_argument('-s', '--server', default='news.gmane.org',
959 help='NNTP server hostname (default: %(default)s)')
960 parser.add_argument('-p', '--port', default=NNTP_PORT, type=int,
961 help='NNTP port number (default: %(default)s)')
962 parser.add_argument('-n', '--nb-articles', default=10, type=int,
963 help='number of articles to fetch (default: %(default)s)')
964 args = parser.parse_args()
965
966 s = NNTP(host=args.server, port=args.port)
967 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000968 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000969
970 def cut(s, lim):
971 if len(s) > lim:
972 s = s[:lim - 4] + "..."
973 return s
974
975 first = str(int(last) - args.nb_articles + 1)
976 resp, overviews = s.xover(first, last)
977 for artnum, over in overviews:
978 author = decode_header(over['from']).split('<', 1)[0]
979 subject = decode_header(over['subject'])
980 lines = int(over[':lines'])
981 print("{:7} {:20} {:42} ({})".format(
982 artnum, cut(author, 20), cut(subject, 42), lines)
983 )
984
985 s.quit()