blob: d5786e2db116e14a544ba02874b9a9e192d28fe7 [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 Pitrou08eeada2010-11-04 21:36:15 +0000574 def list(self, group_pattern=None, *, file=None):
575 """Process a LIST or LIST ACTIVE command. Arguments:
576 - group_pattern: a pattern indicating which groups to query
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000577 - file: Filename string or file object to store the result in
578 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000579 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000580 - list: list of (group, last, first, flag) (strings)
581 """
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000582 if group_pattern is not None:
583 command = 'LIST ACTIVE ' + group_pattern
584 else:
585 command = 'LIST'
586 resp, lines = self._longcmdstring(command, file)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000587 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000588
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000589 def _getdescriptions(self, group_pattern, return_all):
590 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
591 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
592 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
593 if not resp.startswith('215'):
594 # Now the deprecated XGTITLE. This either raises an error
595 # or succeeds with the same output structure as LIST
596 # NEWSGROUPS.
597 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
598 groups = {}
599 for raw_line in lines:
600 match = line_pat.search(raw_line.strip())
601 if match:
602 name, desc = match.group(1, 2)
603 if not return_all:
604 return desc
605 groups[name] = desc
606 if return_all:
607 return resp, groups
608 else:
609 # Nothing found
610 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000611
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000612 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000613 """Get a description for a single group. If more than one
614 group matches ('group' is a pattern), return the first. If no
615 group matches, return an empty string.
616
617 This elides the response code from the server, since it can
618 only be '215' or '285' (for xgtitle) anyway. If the response
619 code is needed, use the 'descriptions' method.
620
621 NOTE: This neither checks for a wildcard in 'group' nor does
622 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000623 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000624
625 def descriptions(self, group_pattern):
626 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000627 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000628
Tim Peters2344fae2001-01-15 00:50:52 +0000629 def group(self, name):
630 """Process a GROUP command. Argument:
631 - group: the group name
632 Returns:
633 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000634 - count: number of articles
635 - first: first article number
636 - last: last article number
637 - name: the group name
638 """
639 resp = self._shortcmd('GROUP ' + name)
640 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000641 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000642 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000643 count = first = last = 0
644 n = len(words)
645 if n > 1:
646 count = words[1]
647 if n > 2:
648 first = words[2]
649 if n > 3:
650 last = words[3]
651 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000652 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000653 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000654
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000655 def help(self, *, file=None):
656 """Process a HELP command. Argument:
657 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000658 Returns:
659 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000660 - list: list of strings returned by the server in response to the
661 HELP command
662 """
663 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000664
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000665 def _statparse(self, resp):
666 """Internal: parse the response line of a STAT, NEXT, LAST,
667 ARTICLE, HEAD or BODY command."""
668 if not resp.startswith('22'):
669 raise NNTPReplyError(resp)
670 words = resp.split()
671 art_num = int(words[1])
672 message_id = words[2]
673 return resp, art_num, message_id
674
675 def _statcmd(self, line):
676 """Internal: process a STAT, NEXT or LAST command."""
677 resp = self._shortcmd(line)
678 return self._statparse(resp)
679
680 def stat(self, message_spec=None):
681 """Process a STAT command. Argument:
682 - message_spec: article number or message id (if not specified,
683 the current article is selected)
684 Returns:
685 - resp: server response if successful
686 - art_num: the article number
687 - message_id: the message id
688 """
689 if message_spec:
690 return self._statcmd('STAT {0}'.format(message_spec))
691 else:
692 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000693
Tim Peters2344fae2001-01-15 00:50:52 +0000694 def next(self):
695 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000696 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000697
Tim Peters2344fae2001-01-15 00:50:52 +0000698 def last(self):
699 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000700 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000701
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000702 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000703 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000704 resp, lines = self._longcmd(line, file)
705 resp, art_num, message_id = self._statparse(resp)
706 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000707
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000708 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000709 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000710 - message_spec: article number or message id
711 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000712 Returns:
713 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000714 - ArticleInfo: (article number, message id, list of header lines)
715 """
716 if message_spec is not None:
717 cmd = 'HEAD {0}'.format(message_spec)
718 else:
719 cmd = 'HEAD'
720 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000721
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000722 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000723 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000724 - message_spec: article number or message id
725 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000726 Returns:
727 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000728 - ArticleInfo: (article number, message id, list of body lines)
729 """
730 if message_spec is not None:
731 cmd = 'BODY {0}'.format(message_spec)
732 else:
733 cmd = 'BODY'
734 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000735
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000736 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000737 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000738 - message_spec: article number or message id
739 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000740 Returns:
741 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000742 - ArticleInfo: (article number, message id, list of article lines)
743 """
744 if message_spec is not None:
745 cmd = 'ARTICLE {0}'.format(message_spec)
746 else:
747 cmd = 'ARTICLE'
748 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000749
Tim Peters2344fae2001-01-15 00:50:52 +0000750 def slave(self):
751 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000752 - resp: server response if successful
753 """
754 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000755
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000756 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000757 """Process an XHDR command (optional server extension). Arguments:
758 - hdr: the header type (e.g. 'subject')
759 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000760 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000761 Returns:
762 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000763 - list: list of (nr, value) strings
764 """
765 pat = re.compile('^([0-9]+) ?(.*)\n?')
766 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
767 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000768 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000769 return m.group(1, 2) if m else line
770 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000771
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000772 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000773 """Process an XOVER command (optional server extension) Arguments:
774 - start: start of range
775 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000776 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000777 Returns:
778 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000779 - list: list of dicts containing the response fields
780 """
781 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
782 file)
783 fmt = self._getoverviewfmt()
784 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000785
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000786 def over(self, message_spec, *, file=None):
787 """Process an OVER command. If the command isn't supported, fall
788 back to XOVER. Arguments:
789 - message_spec:
790 - either a message id, indicating the article to fetch
791 information about
792 - or a (start, end) tuple, indicating a range of article numbers;
793 if end is None, information up to the newest message will be
794 retrieved
795 - or None, indicating the current article number must be used
796 - file: Filename string or file object to store the result in
797 Returns:
798 - resp: server response if successful
799 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000800
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000801 NOTE: the "message id" form isn't supported by XOVER
802 """
803 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
804 if isinstance(message_spec, (tuple, list)):
805 start, end = message_spec
806 cmd += ' {0}-{1}'.format(start, end or '')
807 elif message_spec is not None:
808 cmd = cmd + ' ' + message_spec
809 resp, lines = self._longcmdstring(cmd, file)
810 fmt = self._getoverviewfmt()
811 return resp, _parse_overview(lines, fmt)
812
813 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000814 """Process an XGTITLE command (optional server extension) Arguments:
815 - group: group name wildcard (i.e. news.*)
816 Returns:
817 - resp: server response if successful
818 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000819 warnings.warn("The XGTITLE extension is not actively used, "
820 "use descriptions() instead",
821 PendingDeprecationWarning, 2)
822 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
823 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000824 lines = []
825 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000826 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000827 if match:
828 lines.append(match.group(1, 2))
829 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000830
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000831 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000832 """Process an XPATH command (optional server extension) Arguments:
833 - id: Message id of article
834 Returns:
835 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000836 path: directory path to article
837 """
838 warnings.warn("The XPATH extension is not actively used",
839 PendingDeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000840
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000841 resp = self._shortcmd('XPATH {0}'.format(id))
842 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000843 raise NNTPReplyError(resp)
844 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000845 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000846 except ValueError:
847 raise NNTPReplyError(resp)
848 else:
849 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000850
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000851 def date(self):
852 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000853 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000854 - resp: server response if successful
855 - date: datetime object
856 """
857 resp = self._shortcmd("DATE")
858 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000859 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000860 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000861 if len(elem) != 2:
862 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000863 date = elem[1]
864 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000865 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000866 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000867
Christian Heimes933238a2008-11-05 19:44:21 +0000868 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000869 resp = self._shortcmd(command)
870 # Raises a specific exception if posting is not allowed
871 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000872 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000873 if isinstance(f, (bytes, bytearray)):
874 f = f.splitlines()
875 # We don't use _putline() because:
876 # - we don't want additional CRLF if the file or iterable is already
877 # in the right format
878 # - we don't want a spurious flush() after each line is written
879 for line in f:
880 if not line.endswith(_CRLF):
881 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000882 if line.startswith(b'.'):
883 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000884 self.file.write(line)
885 self.file.write(b".\r\n")
886 self.file.flush()
887 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000888
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000889 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000890 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000891 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000892 Returns:
893 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000894 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000895
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000896 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000897 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000898 - message_id: message-id of the article
899 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000900 Returns:
901 - resp: server response if successful
902 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000903 return self._post('IHAVE {0}'.format(message_id), data)
904
905 def _close(self):
906 self.file.close()
907 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000908
Tim Peters2344fae2001-01-15 00:50:52 +0000909 def quit(self):
910 """Process a QUIT command and close the socket. Returns:
911 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000912 try:
913 resp = self._shortcmd('QUIT')
914 finally:
915 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000916 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000917
918
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000919class NNTP(_NNTPBase):
920
921 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
922 readermode=None, usenetrc=True,
923 timeout=_GLOBAL_DEFAULT_TIMEOUT):
924 """Initialize an instance. Arguments:
925 - host: hostname to connect to
926 - port: port to connect to (default the standard NNTP port)
927 - user: username to authenticate with
928 - password: password to use with username
929 - readermode: if true, send 'mode reader' command after
930 connecting.
931 - usenetrc: allow loading username and password from ~/.netrc file
932 if not specified explicitly
933 - timeout: timeout (in seconds) used for socket connections
934
935 readermode is sometimes necessary if you are connecting to an
936 NNTP server on the local machine and intend to call
937 reader-specific comamnds, such as `group'. If you get
938 unexpected NNTPPermanentErrors, you might need to set
939 readermode.
940 """
941 self.host = host
942 self.port = port
943 self.sock = socket.create_connection((host, port), timeout)
944 file = self.sock.makefile("rwb")
Antoine Pitroua5785b12010-09-29 16:19:50 +0000945 _NNTPBase.__init__(self, file, host, user, password,
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000946 readermode, usenetrc, timeout)
947
948 def _close(self):
949 try:
950 _NNTPBase._close(self)
951 finally:
952 self.sock.close()
953
954
Neal Norwitzef679562002-11-14 02:19:44 +0000955# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000956if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000957 import argparse
958 from email.utils import parsedate
959
960 parser = argparse.ArgumentParser(description="""\
961 nntplib built-in demo - display the latest articles in a newsgroup""")
962 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
963 help='group to fetch messages from (default: %(default)s)')
964 parser.add_argument('-s', '--server', default='news.gmane.org',
965 help='NNTP server hostname (default: %(default)s)')
966 parser.add_argument('-p', '--port', default=NNTP_PORT, type=int,
967 help='NNTP port number (default: %(default)s)')
968 parser.add_argument('-n', '--nb-articles', default=10, type=int,
969 help='number of articles to fetch (default: %(default)s)')
970 args = parser.parse_args()
971
972 s = NNTP(host=args.server, port=args.port)
973 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000974 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000975
976 def cut(s, lim):
977 if len(s) > lim:
978 s = s[:lim - 4] + "..."
979 return s
980
981 first = str(int(last) - args.nb_articles + 1)
982 resp, overviews = s.xover(first, last)
983 for artnum, over in overviews:
984 author = decode_header(over['from']).split('<', 1)[0]
985 subject = decode_header(over['subject'])
986 lines = int(over[':lines'])
987 print("{:7} {:20} {:42} ({})".format(
988 artnum, cut(author, 20), cut(subject, 42), lines)
989 )
990
991 s.quit()