blob: a09c065b8953d92a258f1da9e29320f2d4924325 [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
Antoine Pitroua0781152010-11-05 19:16:37 +0000357 self.nntp_implementation = None
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000358 try:
359 resp, caps = self.capabilities()
360 except NNTPPermanentError:
361 # Server doesn't support capabilities
362 self._caps = {}
363 else:
364 self._caps = caps
365 if 'VERSION' in caps:
Antoine Pitrouf80b3f72010-11-02 22:31:52 +0000366 # The server can advertise several supported versions,
367 # choose the highest.
368 self.nntp_version = max(map(int, caps['VERSION']))
Antoine Pitroua0781152010-11-05 19:16:37 +0000369 if 'IMPLEMENTATION' in caps:
370 self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
Guido van Rossumc629d341992-11-05 10:43:02 +0000371
Tim Peters2344fae2001-01-15 00:50:52 +0000372 def getwelcome(self):
373 """Get the welcome message from the server
374 (this is read and squirreled away by __init__()).
375 If the response code is 200, posting is allowed;
376 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000377
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000378 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000379 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000380
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000381 def getcapabilities(self):
382 """Get the server capabilities, as read by __init__().
383 If the CAPABILITIES command is not supported, an empty dict is
384 returned."""
385 return self._caps
386
Tim Peters2344fae2001-01-15 00:50:52 +0000387 def set_debuglevel(self, level):
388 """Set the debugging level. Argument 'level' means:
389 0: no debugging output (default)
390 1: print commands and responses but not body text etc.
391 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000392
Tim Peters2344fae2001-01-15 00:50:52 +0000393 self.debugging = level
394 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000395
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000396 def _putline(self, line):
397 """Internal: send one line to the server, appending CRLF.
398 The `line` must be a bytes-like object."""
399 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000400 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000401 self.file.write(line)
402 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000403
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000404 def _putcmd(self, line):
405 """Internal: send one command to the server (through _putline()).
406 The `line` must be an unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000407 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000408 line = line.encode(self.encoding, self.errors)
409 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000410
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000411 def _getline(self, strip_crlf=True):
412 """Internal: return one line from the server, stripping _CRLF.
413 Raise EOFError if the connection is closed.
414 Returns a bytes object."""
Tim Peters2344fae2001-01-15 00:50:52 +0000415 line = self.file.readline()
416 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000417 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000418 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000419 if strip_crlf:
420 if line[-2:] == _CRLF:
421 line = line[:-2]
422 elif line[-1:] in _CRLF:
423 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000424 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000425
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000426 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000427 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000428 Raise various errors if the response indicates an error.
429 Returns an unicode string."""
430 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000431 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000432 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000433 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000434 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000435 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000436 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000437 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000438 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000439 raise NNTPProtocolError(resp)
440 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000441
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000442 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000443 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000444 Raise various errors if the response indicates an error.
445
446 Returns a (response, lines) tuple where `response` is an unicode
447 string and `lines` is a list of bytes objects.
448 If `file` is a file-like object, it must be open in binary mode.
449 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000450
451 openedFile = None
452 try:
453 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000454 if isinstance(file, (str, bytes)):
455 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000456
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000457 resp = self._getresp()
458 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000459 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000460
461 lines = []
462 if file is not None:
463 # XXX lines = None instead?
464 terminators = (b'.' + _CRLF, b'.\n')
465 while 1:
466 line = self._getline(False)
467 if line in terminators:
468 break
469 if line.startswith(b'..'):
470 line = line[1:]
471 file.write(line)
472 else:
473 terminator = b'.'
474 while 1:
475 line = self._getline()
476 if line == terminator:
477 break
478 if line.startswith(b'..'):
479 line = line[1:]
480 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000481 finally:
482 # If this method created the file, then it must close it
483 if openedFile:
484 openedFile.close()
485
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000486 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000487
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000488 def _shortcmd(self, line):
489 """Internal: send a command and get the response.
490 Same return value as _getresp()."""
491 self._putcmd(line)
492 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000493
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000494 def _longcmd(self, line, file=None):
495 """Internal: send a command and get the response plus following text.
496 Same return value as _getlongresp()."""
497 self._putcmd(line)
498 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000499
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000500 def _longcmdstring(self, line, file=None):
501 """Internal: send a command and get the response plus following text.
502 Same as _longcmd() and _getlongresp(), except that the returned `lines`
503 are unicode strings rather than bytes objects.
504 """
505 self._putcmd(line)
506 resp, list = self._getlongresp(file)
507 return resp, [line.decode(self.encoding, self.errors)
508 for line in list]
509
510 def _getoverviewfmt(self):
511 """Internal: get the overview format. Queries the server if not
512 already done, else returns the cached value."""
513 try:
514 return self._cachedoverviewfmt
515 except AttributeError:
516 pass
517 try:
518 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
519 except NNTPPermanentError:
520 # Not supported by server?
521 fmt = _DEFAULT_OVERVIEW_FMT[:]
522 else:
523 fmt = _parse_overview_fmt(lines)
524 self._cachedoverviewfmt = fmt
525 return fmt
526
527 def _grouplist(self, lines):
528 # Parse lines into "group last first flag"
529 return [GroupInfo(*line.split()) for line in lines]
530
531 def capabilities(self):
532 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000533 Return:
534 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000535 - caps: a dictionary mapping capability names to lists of tokens
536 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
537 """
538 caps = {}
539 resp, lines = self._longcmdstring("CAPABILITIES")
540 for line in lines:
541 name, *tokens = line.split()
542 caps[name] = tokens
543 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000544
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000545 def newgroups(self, date, *, file=None):
546 """Process a NEWGROUPS command. Arguments:
547 - date: a date or datetime object
548 Return:
549 - resp: server response if successful
550 - list: list of newsgroup names
551 """
552 if not isinstance(date, (datetime.date, datetime.date)):
553 raise TypeError(
554 "the date parameter must be a date or datetime object, "
555 "not '{:40}'".format(date.__class__.__name__))
556 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
557 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
558 resp, lines = self._longcmdstring(cmd, file)
559 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000560
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000561 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000562 """Process a NEWNEWS command. Arguments:
563 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000564 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000565 Return:
566 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000567 - list: list of message ids
568 """
569 if not isinstance(date, (datetime.date, datetime.date)):
570 raise TypeError(
571 "the date parameter must be a date or datetime object, "
572 "not '{:40}'".format(date.__class__.__name__))
573 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
574 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
575 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000576
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000577 def list(self, group_pattern=None, *, file=None):
578 """Process a LIST or LIST ACTIVE command. Arguments:
579 - group_pattern: a pattern indicating which groups to query
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000580 - file: Filename string or file object to store the result in
581 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000582 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000583 - list: list of (group, last, first, flag) (strings)
584 """
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000585 if group_pattern is not None:
586 command = 'LIST ACTIVE ' + group_pattern
587 else:
588 command = 'LIST'
589 resp, lines = self._longcmdstring(command, file)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000590 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000591
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000592 def _getdescriptions(self, group_pattern, return_all):
593 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
594 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
595 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
596 if not resp.startswith('215'):
597 # Now the deprecated XGTITLE. This either raises an error
598 # or succeeds with the same output structure as LIST
599 # NEWSGROUPS.
600 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
601 groups = {}
602 for raw_line in lines:
603 match = line_pat.search(raw_line.strip())
604 if match:
605 name, desc = match.group(1, 2)
606 if not return_all:
607 return desc
608 groups[name] = desc
609 if return_all:
610 return resp, groups
611 else:
612 # Nothing found
613 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000614
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000615 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000616 """Get a description for a single group. If more than one
617 group matches ('group' is a pattern), return the first. If no
618 group matches, return an empty string.
619
620 This elides the response code from the server, since it can
621 only be '215' or '285' (for xgtitle) anyway. If the response
622 code is needed, use the 'descriptions' method.
623
624 NOTE: This neither checks for a wildcard in 'group' nor does
625 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000626 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000627
628 def descriptions(self, group_pattern):
629 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000630 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000631
Tim Peters2344fae2001-01-15 00:50:52 +0000632 def group(self, name):
633 """Process a GROUP command. Argument:
634 - group: the group name
635 Returns:
636 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000637 - count: number of articles
638 - first: first article number
639 - last: last article number
640 - name: the group name
641 """
642 resp = self._shortcmd('GROUP ' + name)
643 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000644 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000645 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000646 count = first = last = 0
647 n = len(words)
648 if n > 1:
649 count = words[1]
650 if n > 2:
651 first = words[2]
652 if n > 3:
653 last = words[3]
654 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000655 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000656 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000657
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000658 def help(self, *, file=None):
659 """Process a HELP command. Argument:
660 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000661 Returns:
662 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000663 - list: list of strings returned by the server in response to the
664 HELP command
665 """
666 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000667
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000668 def _statparse(self, resp):
669 """Internal: parse the response line of a STAT, NEXT, LAST,
670 ARTICLE, HEAD or BODY command."""
671 if not resp.startswith('22'):
672 raise NNTPReplyError(resp)
673 words = resp.split()
674 art_num = int(words[1])
675 message_id = words[2]
676 return resp, art_num, message_id
677
678 def _statcmd(self, line):
679 """Internal: process a STAT, NEXT or LAST command."""
680 resp = self._shortcmd(line)
681 return self._statparse(resp)
682
683 def stat(self, message_spec=None):
684 """Process a STAT command. Argument:
685 - message_spec: article number or message id (if not specified,
686 the current article is selected)
687 Returns:
688 - resp: server response if successful
689 - art_num: the article number
690 - message_id: the message id
691 """
692 if message_spec:
693 return self._statcmd('STAT {0}'.format(message_spec))
694 else:
695 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000696
Tim Peters2344fae2001-01-15 00:50:52 +0000697 def next(self):
698 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000699 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000700
Tim Peters2344fae2001-01-15 00:50:52 +0000701 def last(self):
702 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000703 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000704
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000705 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000706 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000707 resp, lines = self._longcmd(line, file)
708 resp, art_num, message_id = self._statparse(resp)
709 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000710
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000711 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000712 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000713 - message_spec: article number or message id
714 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000715 Returns:
716 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000717 - ArticleInfo: (article number, message id, list of header lines)
718 """
719 if message_spec is not None:
720 cmd = 'HEAD {0}'.format(message_spec)
721 else:
722 cmd = 'HEAD'
723 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000724
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000725 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000726 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000727 - message_spec: article number or message id
728 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000729 Returns:
730 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000731 - ArticleInfo: (article number, message id, list of body lines)
732 """
733 if message_spec is not None:
734 cmd = 'BODY {0}'.format(message_spec)
735 else:
736 cmd = 'BODY'
737 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000738
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000739 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000740 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000741 - message_spec: article number or message id
742 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000743 Returns:
744 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000745 - ArticleInfo: (article number, message id, list of article lines)
746 """
747 if message_spec is not None:
748 cmd = 'ARTICLE {0}'.format(message_spec)
749 else:
750 cmd = 'ARTICLE'
751 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000752
Tim Peters2344fae2001-01-15 00:50:52 +0000753 def slave(self):
754 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000755 - resp: server response if successful
756 """
757 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000758
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000759 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000760 """Process an XHDR command (optional server extension). Arguments:
761 - hdr: the header type (e.g. 'subject')
762 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000763 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000764 Returns:
765 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000766 - list: list of (nr, value) strings
767 """
768 pat = re.compile('^([0-9]+) ?(.*)\n?')
769 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
770 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000771 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000772 return m.group(1, 2) if m else line
773 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000774
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000775 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000776 """Process an XOVER command (optional server extension) Arguments:
777 - start: start of range
778 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000779 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000780 Returns:
781 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000782 - list: list of dicts containing the response fields
783 """
784 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
785 file)
786 fmt = self._getoverviewfmt()
787 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000788
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000789 def over(self, message_spec, *, file=None):
790 """Process an OVER command. If the command isn't supported, fall
791 back to XOVER. Arguments:
792 - message_spec:
793 - either a message id, indicating the article to fetch
794 information about
795 - or a (start, end) tuple, indicating a range of article numbers;
796 if end is None, information up to the newest message will be
797 retrieved
798 - or None, indicating the current article number must be used
799 - file: Filename string or file object to store the result in
800 Returns:
801 - resp: server response if successful
802 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000803
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000804 NOTE: the "message id" form isn't supported by XOVER
805 """
806 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
807 if isinstance(message_spec, (tuple, list)):
808 start, end = message_spec
809 cmd += ' {0}-{1}'.format(start, end or '')
810 elif message_spec is not None:
811 cmd = cmd + ' ' + message_spec
812 resp, lines = self._longcmdstring(cmd, file)
813 fmt = self._getoverviewfmt()
814 return resp, _parse_overview(lines, fmt)
815
816 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000817 """Process an XGTITLE command (optional server extension) Arguments:
818 - group: group name wildcard (i.e. news.*)
819 Returns:
820 - resp: server response if successful
821 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000822 warnings.warn("The XGTITLE extension is not actively used, "
823 "use descriptions() instead",
824 PendingDeprecationWarning, 2)
825 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
826 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000827 lines = []
828 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000829 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000830 if match:
831 lines.append(match.group(1, 2))
832 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000833
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000834 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000835 """Process an XPATH command (optional server extension) Arguments:
836 - id: Message id of article
837 Returns:
838 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000839 path: directory path to article
840 """
841 warnings.warn("The XPATH extension is not actively used",
842 PendingDeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000843
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000844 resp = self._shortcmd('XPATH {0}'.format(id))
845 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000846 raise NNTPReplyError(resp)
847 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000848 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000849 except ValueError:
850 raise NNTPReplyError(resp)
851 else:
852 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000853
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000854 def date(self):
855 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000856 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000857 - resp: server response if successful
858 - date: datetime object
859 """
860 resp = self._shortcmd("DATE")
861 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000862 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000863 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000864 if len(elem) != 2:
865 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000866 date = elem[1]
867 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000868 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000869 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000870
Christian Heimes933238a2008-11-05 19:44:21 +0000871 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000872 resp = self._shortcmd(command)
873 # Raises a specific exception if posting is not allowed
874 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000875 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000876 if isinstance(f, (bytes, bytearray)):
877 f = f.splitlines()
878 # We don't use _putline() because:
879 # - we don't want additional CRLF if the file or iterable is already
880 # in the right format
881 # - we don't want a spurious flush() after each line is written
882 for line in f:
883 if not line.endswith(_CRLF):
884 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000885 if line.startswith(b'.'):
886 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000887 self.file.write(line)
888 self.file.write(b".\r\n")
889 self.file.flush()
890 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000891
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000892 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000893 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000894 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000895 Returns:
896 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000897 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000898
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000899 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000900 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000901 - message_id: message-id of the article
902 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000903 Returns:
904 - resp: server response if successful
905 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000906 return self._post('IHAVE {0}'.format(message_id), data)
907
908 def _close(self):
909 self.file.close()
910 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000911
Tim Peters2344fae2001-01-15 00:50:52 +0000912 def quit(self):
913 """Process a QUIT command and close the socket. Returns:
914 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000915 try:
916 resp = self._shortcmd('QUIT')
917 finally:
918 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000919 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000920
921
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000922class NNTP(_NNTPBase):
923
924 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
925 readermode=None, usenetrc=True,
926 timeout=_GLOBAL_DEFAULT_TIMEOUT):
927 """Initialize an instance. Arguments:
928 - host: hostname to connect to
929 - port: port to connect to (default the standard NNTP port)
930 - user: username to authenticate with
931 - password: password to use with username
932 - readermode: if true, send 'mode reader' command after
933 connecting.
934 - usenetrc: allow loading username and password from ~/.netrc file
935 if not specified explicitly
936 - timeout: timeout (in seconds) used for socket connections
937
938 readermode is sometimes necessary if you are connecting to an
939 NNTP server on the local machine and intend to call
940 reader-specific comamnds, such as `group'. If you get
941 unexpected NNTPPermanentErrors, you might need to set
942 readermode.
943 """
944 self.host = host
945 self.port = port
946 self.sock = socket.create_connection((host, port), timeout)
947 file = self.sock.makefile("rwb")
Antoine Pitroua5785b12010-09-29 16:19:50 +0000948 _NNTPBase.__init__(self, file, host, user, password,
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000949 readermode, usenetrc, timeout)
950
951 def _close(self):
952 try:
953 _NNTPBase._close(self)
954 finally:
955 self.sock.close()
956
957
Neal Norwitzef679562002-11-14 02:19:44 +0000958# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +0000959if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000960 import argparse
961 from email.utils import parsedate
962
963 parser = argparse.ArgumentParser(description="""\
964 nntplib built-in demo - display the latest articles in a newsgroup""")
965 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
966 help='group to fetch messages from (default: %(default)s)')
967 parser.add_argument('-s', '--server', default='news.gmane.org',
968 help='NNTP server hostname (default: %(default)s)')
969 parser.add_argument('-p', '--port', default=NNTP_PORT, type=int,
970 help='NNTP port number (default: %(default)s)')
971 parser.add_argument('-n', '--nb-articles', default=10, type=int,
972 help='number of articles to fetch (default: %(default)s)')
973 args = parser.parse_args()
974
975 s = NNTP(host=args.server, port=args.port)
976 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000977 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000978
979 def cut(s, lim):
980 if len(s) > lim:
981 s = s[:lim - 4] + "..."
982 return s
983
984 first = str(int(last) - args.nb_articles + 1)
985 resp, overviews = s.xover(first, last)
986 for artnum, over in overviews:
987 author = decode_header(over['from']).split('<', 1)[0]
988 subject = decode_header(over['subject'])
989 lines = int(over[':lines'])
990 print("{:7} {:20} {:42} ({})".format(
991 artnum, cut(author, 20), cut(subject, 42), lines)
992 )
993
994 s.quit()