blob: fde339a7f3df064ade805adf46cf6233043a2369 [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
Antoine Pitrou4103bc02010-11-03 18:18:43 +0000208 # (unless the field is totally empty)
209 h = field_name + ": "
210 if token and token[:len(h)].lower() != h:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000211 raise NNTPDataError("OVER/XOVER response doesn't include "
212 "names of additional headers")
Antoine Pitrou4103bc02010-11-03 18:18:43 +0000213 token = token[len(h):] if token else None
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000214 fields[fmt[i]] = token
215 overview.append((article_number, fields))
216 return overview
217
218def _parse_datetime(date_str, time_str=None):
219 """Parse a pair of (date, time) strings, and return a datetime object.
220 If only the date is given, it is assumed to be date and time
221 concatenated together (e.g. response to the DATE command).
222 """
223 if time_str is None:
224 time_str = date_str[-6:]
225 date_str = date_str[:-6]
226 hours = int(time_str[:2])
227 minutes = int(time_str[2:4])
228 seconds = int(time_str[4:])
229 year = int(date_str[:-4])
230 month = int(date_str[-4:-2])
231 day = int(date_str[-2:])
232 # RFC 3977 doesn't say how to interpret 2-char years. Assume that
233 # there are no dates before 1970 on Usenet.
234 if year < 70:
235 year += 2000
236 elif year < 100:
237 year += 1900
238 return datetime.datetime(year, month, day, hours, minutes, seconds)
239
240def _unparse_datetime(dt, legacy=False):
241 """Format a date or datetime object as a pair of (date, time) strings
242 in the format required by the NEWNEWS and NEWGROUPS commands. If a
243 date object is passed, the time is assumed to be midnight (00h00).
244
245 The returned representation depends on the legacy flag:
246 * if legacy is False (the default):
247 date has the YYYYMMDD format and time the HHMMSS format
248 * if legacy is True:
249 date has the YYMMDD format and time the HHMMSS format.
250 RFC 3977 compliant servers should understand both formats; therefore,
251 legacy is only needed when talking to old servers.
252 """
253 if not isinstance(dt, datetime.datetime):
254 time_str = "000000"
255 else:
256 time_str = "{0.hour:02d}{0.minute:02d}{0.second:02d}".format(dt)
257 y = dt.year
258 if legacy:
259 y = y % 100
260 date_str = "{0:02d}{1.month:02d}{1.day:02d}".format(y, dt)
261 else:
262 date_str = "{0:04d}{1.month:02d}{1.day:02d}".format(y, dt)
263 return date_str, time_str
264
265
266# The classes themselves
267class _NNTPBase:
268 # UTF-8 is the character set for all NNTP commands and responses: they
269 # are automatically encoded (when sending) and decoded (and receiving)
270 # by this class.
271 # However, some multi-line data blocks can contain arbitrary bytes (for
272 # example, latin-1 or utf-16 data in the body of a message). Commands
273 # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
274 # data will therefore only accept and produce bytes objects.
275 # Furthermore, since there could be non-compliant servers out there,
276 # we use 'surrogateescape' as the error handler for fault tolerance
277 # and easy round-tripping. This could be useful for some applications
278 # (e.g. NNTP gateways).
279
280 encoding = 'utf-8'
281 errors = 'surrogateescape'
282
Antoine Pitroua5785b12010-09-29 16:19:50 +0000283 def __init__(self, file, host, user=None, password=None,
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000284 readermode=None, usenetrc=True,
285 timeout=_GLOBAL_DEFAULT_TIMEOUT):
Tim Peters2344fae2001-01-15 00:50:52 +0000286 """Initialize an instance. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000287 - file: file-like object (open for read/write in binary mode)
Antoine Pitroua5785b12010-09-29 16:19:50 +0000288 - host: hostname of the server (used if `usenetrc` is True)
Tim Peters2344fae2001-01-15 00:50:52 +0000289 - user: username to authenticate with
290 - password: password to use with username
291 - readermode: if true, send 'mode reader' command after
292 connecting.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000293 - usenetrc: allow loading username and password from ~/.netrc file
294 if not specified explicitly
295 - timeout: timeout (in seconds) used for socket connections
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000296
Tim Peters2344fae2001-01-15 00:50:52 +0000297 readermode is sometimes necessary if you are connecting to an
298 NNTP server on the local machine and intend to call
299 reader-specific comamnds, such as `group'. If you get
300 unexpected NNTPPermanentErrors, you might need to set
301 readermode.
302 """
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000303 self.file = file
Tim Peters2344fae2001-01-15 00:50:52 +0000304 self.debugging = 0
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000305 self.welcome = self._getresp()
Tim Petersdfb673b2001-01-16 07:12:46 +0000306
Thomas Wouters47adcba2001-01-16 06:35:14 +0000307 # 'mode reader' is sometimes necessary to enable 'reader' mode.
Tim Petersdfb673b2001-01-16 07:12:46 +0000308 # However, the order in which 'mode reader' and 'authinfo' need to
Thomas Wouters47adcba2001-01-16 06:35:14 +0000309 # arrive differs between some NNTP servers. Try to send
310 # 'mode reader', and if it fails with an authorization failed
311 # error, try again after sending authinfo.
312 readermode_afterauth = 0
Tim Peters2344fae2001-01-15 00:50:52 +0000313 if readermode:
314 try:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000315 self.welcome = self._shortcmd('mode reader')
Tim Peters2344fae2001-01-15 00:50:52 +0000316 except NNTPPermanentError:
317 # error 500, probably 'not implemented'
318 pass
Guido van Rossumb940e112007-01-10 16:19:56 +0000319 except NNTPTemporaryError as e:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000320 if user and e.response.startswith('480'):
Thomas Wouters47adcba2001-01-16 06:35:14 +0000321 # Need authorization before 'mode reader'
322 readermode_afterauth = 1
323 else:
324 raise
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000325 # If no login/password was specified, try to get them from ~/.netrc
326 # Presume that if .netc has an entry, NNRP authentication is required.
Eric S. Raymond782d9402002-11-17 17:53:12 +0000327 try:
Martin v. Löwis9513e342004-08-03 14:36:32 +0000328 if usenetrc and not user:
Eric S. Raymond782d9402002-11-17 17:53:12 +0000329 import netrc
330 credentials = netrc.netrc()
331 auth = credentials.authenticators(host)
332 if auth:
333 user = auth[0]
334 password = auth[2]
335 except IOError:
336 pass
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000337 # Perform NNTP authentication if needed.
Tim Peters2344fae2001-01-15 00:50:52 +0000338 if user:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000339 resp = self._shortcmd('authinfo user '+user)
340 if resp.startswith('381'):
Tim Peters2344fae2001-01-15 00:50:52 +0000341 if not password:
342 raise NNTPReplyError(resp)
343 else:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000344 resp = self._shortcmd(
Tim Peters2344fae2001-01-15 00:50:52 +0000345 'authinfo pass '+password)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000346 if not resp.startswith('281'):
Tim Peters2344fae2001-01-15 00:50:52 +0000347 raise NNTPPermanentError(resp)
Thomas Wouters47adcba2001-01-16 06:35:14 +0000348 if readermode_afterauth:
349 try:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000350 self.welcome = self._shortcmd('mode reader')
Thomas Wouters47adcba2001-01-16 06:35:14 +0000351 except NNTPPermanentError:
352 # error 500, probably 'not implemented'
353 pass
Tim Petersdfb673b2001-01-16 07:12:46 +0000354
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000355 # Inquire about capabilities (RFC 3977)
356 self.nntp_version = 1
357 try:
358 resp, caps = self.capabilities()
359 except NNTPPermanentError:
360 # Server doesn't support capabilities
361 self._caps = {}
362 else:
363 self._caps = caps
364 if 'VERSION' in caps:
Antoine Pitrouf80b3f72010-11-02 22:31:52 +0000365 # The server can advertise several supported versions,
366 # choose the highest.
367 self.nntp_version = max(map(int, caps['VERSION']))
Guido van Rossumc629d341992-11-05 10:43:02 +0000368
Tim Peters2344fae2001-01-15 00:50:52 +0000369 def getwelcome(self):
370 """Get the welcome message from the server
371 (this is read and squirreled away by __init__()).
372 If the response code is 200, posting is allowed;
373 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000374
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000375 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000376 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000377
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000378 def getcapabilities(self):
379 """Get the server capabilities, as read by __init__().
380 If the CAPABILITIES command is not supported, an empty dict is
381 returned."""
382 return self._caps
383
Tim Peters2344fae2001-01-15 00:50:52 +0000384 def set_debuglevel(self, level):
385 """Set the debugging level. Argument 'level' means:
386 0: no debugging output (default)
387 1: print commands and responses but not body text etc.
388 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000389
Tim Peters2344fae2001-01-15 00:50:52 +0000390 self.debugging = level
391 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000392
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000393 def _putline(self, line):
394 """Internal: send one line to the server, appending CRLF.
395 The `line` must be a bytes-like object."""
396 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000397 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000398 self.file.write(line)
399 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000400
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000401 def _putcmd(self, line):
402 """Internal: send one command to the server (through _putline()).
403 The `line` must be an unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000404 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000405 line = line.encode(self.encoding, self.errors)
406 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000407
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000408 def _getline(self, strip_crlf=True):
409 """Internal: return one line from the server, stripping _CRLF.
410 Raise EOFError if the connection is closed.
411 Returns a bytes object."""
Tim Peters2344fae2001-01-15 00:50:52 +0000412 line = self.file.readline()
413 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000414 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000415 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000416 if strip_crlf:
417 if line[-2:] == _CRLF:
418 line = line[:-2]
419 elif line[-1:] in _CRLF:
420 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000421 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000422
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000423 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000424 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000425 Raise various errors if the response indicates an error.
426 Returns an unicode string."""
427 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000428 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000429 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000430 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000431 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000432 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000433 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000434 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000435 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000436 raise NNTPProtocolError(resp)
437 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000438
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000439 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000440 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000441 Raise various errors if the response indicates an error.
442
443 Returns a (response, lines) tuple where `response` is an unicode
444 string and `lines` is a list of bytes objects.
445 If `file` is a file-like object, it must be open in binary mode.
446 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000447
448 openedFile = None
449 try:
450 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000451 if isinstance(file, (str, bytes)):
452 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000453
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000454 resp = self._getresp()
455 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000456 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000457
458 lines = []
459 if file is not None:
460 # XXX lines = None instead?
461 terminators = (b'.' + _CRLF, b'.\n')
462 while 1:
463 line = self._getline(False)
464 if line in terminators:
465 break
466 if line.startswith(b'..'):
467 line = line[1:]
468 file.write(line)
469 else:
470 terminator = b'.'
471 while 1:
472 line = self._getline()
473 if line == terminator:
474 break
475 if line.startswith(b'..'):
476 line = line[1:]
477 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000478 finally:
479 # If this method created the file, then it must close it
480 if openedFile:
481 openedFile.close()
482
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000483 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000484
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000485 def _shortcmd(self, line):
486 """Internal: send a command and get the response.
487 Same return value as _getresp()."""
488 self._putcmd(line)
489 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000490
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000491 def _longcmd(self, line, file=None):
492 """Internal: send a command and get the response plus following text.
493 Same return value as _getlongresp()."""
494 self._putcmd(line)
495 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000496
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000497 def _longcmdstring(self, line, file=None):
498 """Internal: send a command and get the response plus following text.
499 Same as _longcmd() and _getlongresp(), except that the returned `lines`
500 are unicode strings rather than bytes objects.
501 """
502 self._putcmd(line)
503 resp, list = self._getlongresp(file)
504 return resp, [line.decode(self.encoding, self.errors)
505 for line in list]
506
507 def _getoverviewfmt(self):
508 """Internal: get the overview format. Queries the server if not
509 already done, else returns the cached value."""
510 try:
511 return self._cachedoverviewfmt
512 except AttributeError:
513 pass
514 try:
515 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
516 except NNTPPermanentError:
517 # Not supported by server?
518 fmt = _DEFAULT_OVERVIEW_FMT[:]
519 else:
520 fmt = _parse_overview_fmt(lines)
521 self._cachedoverviewfmt = fmt
522 return fmt
523
524 def _grouplist(self, lines):
525 # Parse lines into "group last first flag"
526 return [GroupInfo(*line.split()) for line in lines]
527
528 def capabilities(self):
529 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000530 Return:
531 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000532 - caps: a dictionary mapping capability names to lists of tokens
533 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
534 """
535 caps = {}
536 resp, lines = self._longcmdstring("CAPABILITIES")
537 for line in lines:
538 name, *tokens = line.split()
539 caps[name] = tokens
540 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000541
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000542 def newgroups(self, date, *, file=None):
543 """Process a NEWGROUPS command. Arguments:
544 - date: a date or datetime object
545 Return:
546 - resp: server response if successful
547 - list: list of newsgroup names
548 """
549 if not isinstance(date, (datetime.date, datetime.date)):
550 raise TypeError(
551 "the date parameter must be a date or datetime object, "
552 "not '{:40}'".format(date.__class__.__name__))
553 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
554 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
555 resp, lines = self._longcmdstring(cmd, file)
556 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000557
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000558 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000559 """Process a NEWNEWS command. Arguments:
560 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000561 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000562 Return:
563 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000564 - list: list of message ids
565 """
566 if not isinstance(date, (datetime.date, datetime.date)):
567 raise TypeError(
568 "the date parameter must be a date or datetime object, "
569 "not '{:40}'".format(date.__class__.__name__))
570 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
571 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
572 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000573
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000574 def list(self, *, file=None):
575 """Process a LIST command. Argument:
576 - file: Filename string or file object to store the result in
577 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000578 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000579 - list: list of (group, last, first, flag) (strings)
580 """
581 resp, lines = self._longcmdstring('LIST', file)
582 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000583
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000584 def _getdescriptions(self, group_pattern, return_all):
585 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
586 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
587 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
588 if not resp.startswith('215'):
589 # Now the deprecated XGTITLE. This either raises an error
590 # or succeeds with the same output structure as LIST
591 # NEWSGROUPS.
592 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
593 groups = {}
594 for raw_line in lines:
595 match = line_pat.search(raw_line.strip())
596 if match:
597 name, desc = match.group(1, 2)
598 if not return_all:
599 return desc
600 groups[name] = desc
601 if return_all:
602 return resp, groups
603 else:
604 # Nothing found
605 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000606
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000607 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000608 """Get a description for a single group. If more than one
609 group matches ('group' is a pattern), return the first. If no
610 group matches, return an empty string.
611
612 This elides the response code from the server, since it can
613 only be '215' or '285' (for xgtitle) anyway. If the response
614 code is needed, use the 'descriptions' method.
615
616 NOTE: This neither checks for a wildcard in 'group' nor does
617 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000618 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000619
620 def descriptions(self, group_pattern):
621 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000622 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000623
Tim Peters2344fae2001-01-15 00:50:52 +0000624 def group(self, name):
625 """Process a GROUP command. Argument:
626 - group: the group name
627 Returns:
628 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000629 - count: number of articles
630 - first: first article number
631 - last: last article number
632 - name: the group name
633 """
634 resp = self._shortcmd('GROUP ' + name)
635 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000636 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000637 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000638 count = first = last = 0
639 n = len(words)
640 if n > 1:
641 count = words[1]
642 if n > 2:
643 first = words[2]
644 if n > 3:
645 last = words[3]
646 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000647 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000648 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000649
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000650 def help(self, *, file=None):
651 """Process a HELP command. Argument:
652 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000653 Returns:
654 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000655 - list: list of strings returned by the server in response to the
656 HELP command
657 """
658 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000659
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000660 def _statparse(self, resp):
661 """Internal: parse the response line of a STAT, NEXT, LAST,
662 ARTICLE, HEAD or BODY command."""
663 if not resp.startswith('22'):
664 raise NNTPReplyError(resp)
665 words = resp.split()
666 art_num = int(words[1])
667 message_id = words[2]
668 return resp, art_num, message_id
669
670 def _statcmd(self, line):
671 """Internal: process a STAT, NEXT or LAST command."""
672 resp = self._shortcmd(line)
673 return self._statparse(resp)
674
675 def stat(self, message_spec=None):
676 """Process a STAT command. Argument:
677 - message_spec: article number or message id (if not specified,
678 the current article is selected)
679 Returns:
680 - resp: server response if successful
681 - art_num: the article number
682 - message_id: the message id
683 """
684 if message_spec:
685 return self._statcmd('STAT {0}'.format(message_spec))
686 else:
687 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000688
Tim Peters2344fae2001-01-15 00:50:52 +0000689 def next(self):
690 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000691 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000692
Tim Peters2344fae2001-01-15 00:50:52 +0000693 def last(self):
694 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000695 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000696
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000697 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000698 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000699 resp, lines = self._longcmd(line, file)
700 resp, art_num, message_id = self._statparse(resp)
701 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000702
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000703 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000704 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000705 - message_spec: article number or message id
706 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000707 Returns:
708 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000709 - ArticleInfo: (article number, message id, list of header lines)
710 """
711 if message_spec is not None:
712 cmd = 'HEAD {0}'.format(message_spec)
713 else:
714 cmd = 'HEAD'
715 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000716
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000717 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000718 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000719 - message_spec: article number or message id
720 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000721 Returns:
722 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000723 - ArticleInfo: (article number, message id, list of body lines)
724 """
725 if message_spec is not None:
726 cmd = 'BODY {0}'.format(message_spec)
727 else:
728 cmd = 'BODY'
729 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000730
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000731 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000732 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000733 - message_spec: article number or message id
734 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000735 Returns:
736 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000737 - ArticleInfo: (article number, message id, list of article lines)
738 """
739 if message_spec is not None:
740 cmd = 'ARTICLE {0}'.format(message_spec)
741 else:
742 cmd = 'ARTICLE'
743 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000744
Tim Peters2344fae2001-01-15 00:50:52 +0000745 def slave(self):
746 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000747 - resp: server response if successful
748 """
749 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000750
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000751 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000752 """Process an XHDR command (optional server extension). Arguments:
753 - hdr: the header type (e.g. 'subject')
754 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000755 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000756 Returns:
757 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000758 - list: list of (nr, value) strings
759 """
760 pat = re.compile('^([0-9]+) ?(.*)\n?')
761 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
762 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000763 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000764 return m.group(1, 2) if m else line
765 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000766
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000767 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000768 """Process an XOVER command (optional server extension) Arguments:
769 - start: start of range
770 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000771 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000772 Returns:
773 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000774 - list: list of dicts containing the response fields
775 """
776 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
777 file)
778 fmt = self._getoverviewfmt()
779 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000780
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000781 def over(self, message_spec, *, file=None):
782 """Process an OVER command. If the command isn't supported, fall
783 back to XOVER. Arguments:
784 - message_spec:
785 - either a message id, indicating the article to fetch
786 information about
787 - or a (start, end) tuple, indicating a range of article numbers;
788 if end is None, information up to the newest message will be
789 retrieved
790 - or None, indicating the current article number must be used
791 - file: Filename string or file object to store the result in
792 Returns:
793 - resp: server response if successful
794 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000795
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000796 NOTE: the "message id" form isn't supported by XOVER
797 """
798 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
799 if isinstance(message_spec, (tuple, list)):
800 start, end = message_spec
801 cmd += ' {0}-{1}'.format(start, end or '')
802 elif message_spec is not None:
803 cmd = cmd + ' ' + message_spec
804 resp, lines = self._longcmdstring(cmd, file)
805 fmt = self._getoverviewfmt()
806 return resp, _parse_overview(lines, fmt)
807
808 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000809 """Process an XGTITLE command (optional server extension) Arguments:
810 - group: group name wildcard (i.e. news.*)
811 Returns:
812 - resp: server response if successful
813 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000814 warnings.warn("The XGTITLE extension is not actively used, "
815 "use descriptions() instead",
816 PendingDeprecationWarning, 2)
817 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
818 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000819 lines = []
820 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000821 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000822 if match:
823 lines.append(match.group(1, 2))
824 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000825
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000826 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000827 """Process an XPATH command (optional server extension) Arguments:
828 - id: Message id of article
829 Returns:
830 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000831 path: directory path to article
832 """
833 warnings.warn("The XPATH extension is not actively used",
834 PendingDeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000835
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000836 resp = self._shortcmd('XPATH {0}'.format(id))
837 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000838 raise NNTPReplyError(resp)
839 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000840 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000841 except ValueError:
842 raise NNTPReplyError(resp)
843 else:
844 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000845
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000846 def date(self):
847 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000848 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000849 - resp: server response if successful
850 - date: datetime object
851 """
852 resp = self._shortcmd("DATE")
853 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000854 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000855 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000856 if len(elem) != 2:
857 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000858 date = elem[1]
859 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000860 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000861 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000862
Christian Heimes933238a2008-11-05 19:44:21 +0000863 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000864 resp = self._shortcmd(command)
865 # Raises a specific exception if posting is not allowed
866 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000867 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000868 if isinstance(f, (bytes, bytearray)):
869 f = f.splitlines()
870 # We don't use _putline() because:
871 # - we don't want additional CRLF if the file or iterable is already
872 # in the right format
873 # - we don't want a spurious flush() after each line is written
874 for line in f:
875 if not line.endswith(_CRLF):
876 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000877 if line.startswith(b'.'):
878 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000879 self.file.write(line)
880 self.file.write(b".\r\n")
881 self.file.flush()
882 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000883
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000884 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000885 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000886 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000887 Returns:
888 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000889 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000890
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000891 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000892 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000893 - message_id: message-id of the article
894 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000895 Returns:
896 - resp: server response if successful
897 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000898 return self._post('IHAVE {0}'.format(message_id), data)
899
900 def _close(self):
901 self.file.close()
902 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000903
Tim Peters2344fae2001-01-15 00:50:52 +0000904 def quit(self):
905 """Process a QUIT command and close the socket. Returns:
906 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000907 try:
908 resp = self._shortcmd('QUIT')
909 finally:
910 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000911 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000912
913
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000914class NNTP(_NNTPBase):
915
916 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
917 readermode=None, usenetrc=True,
918 timeout=_GLOBAL_DEFAULT_TIMEOUT):
919 """Initialize an instance. Arguments:
920 - host: hostname to connect to
921 - port: port to connect to (default the standard NNTP port)
922 - user: username to authenticate with
923 - password: password to use with username
924 - readermode: if true, send 'mode reader' command after
925 connecting.
926 - usenetrc: allow loading username and password from ~/.netrc file
927 if not specified explicitly
928 - timeout: timeout (in seconds) used for socket connections
929
930 readermode is sometimes necessary if you are connecting to an
931 NNTP server on the local machine and intend to call
932 reader-specific comamnds, such as `group'. If you get
933 unexpected NNTPPermanentErrors, you might need to set
934 readermode.
935 """
936 self.host = host
937 self.port = port
938 self.sock = socket.create_connection((host, port), timeout)
939 file = self.sock.makefile("rwb")
Antoine Pitroua5785b12010-09-29 16:19:50 +0000940 _NNTPBase.__init__(self, file, host, user, password,
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000941 readermode, usenetrc, timeout)
942
943 def _close(self):
944 try:
945 _NNTPBase._close(self)
946 finally:
947 self.sock.close()
948
949
Neal Norwitzef679562002-11-14 02:19:44 +0000950# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000951if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000952 import argparse
953 from email.utils import parsedate
954
955 parser = argparse.ArgumentParser(description="""\
956 nntplib built-in demo - display the latest articles in a newsgroup""")
957 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
958 help='group to fetch messages from (default: %(default)s)')
959 parser.add_argument('-s', '--server', default='news.gmane.org',
960 help='NNTP server hostname (default: %(default)s)')
961 parser.add_argument('-p', '--port', default=NNTP_PORT, type=int,
962 help='NNTP port number (default: %(default)s)')
963 parser.add_argument('-n', '--nb-articles', default=10, type=int,
964 help='number of articles to fetch (default: %(default)s)')
965 args = parser.parse_args()
966
967 s = NNTP(host=args.server, port=args.port)
968 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000969 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000970
971 def cut(s, lim):
972 if len(s) > lim:
973 s = s[:lim - 4] + "..."
974 return s
975
976 first = str(int(last) - args.nb_articles + 1)
977 resp, overviews = s.xover(first, last)
978 for artnum, over in overviews:
979 author = decode_header(over['from']).split('<', 1)[0]
980 subject = decode_header(over['subject'])
981 lines = int(over[':lines'])
982 print("{:7} {:20} {:42} ({})".format(
983 artnum, cut(author, 20), cut(subject, 42), lines)
984 )
985
986 s.quit()