blob: 3e863dc7400f93ad72248e9a45fc9d24b64c768e [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 Pitrou1cb121e2010-11-09 18:54:37 +000072try:
73 import ssl
74except ImportError:
75 _have_ssl = False
76else:
77 _have_ssl = True
78
Antoine Pitrou69ab9512010-09-29 15:03:40 +000079from email.header import decode_header as _email_decode_header
80from socket import _GLOBAL_DEFAULT_TIMEOUT
81
82__all__ = ["NNTP",
83 "NNTPReplyError", "NNTPTemporaryError", "NNTPPermanentError",
84 "NNTPProtocolError", "NNTPDataError",
85 "decode_header",
86 ]
Tim Peters2344fae2001-01-15 00:50:52 +000087
Barry Warsaw9dd78722000-02-10 20:25:53 +000088# Exceptions raised when an error or invalid response is received
89class NNTPError(Exception):
Tim Peters2344fae2001-01-15 00:50:52 +000090 """Base class for all nntplib exceptions"""
91 def __init__(self, *args):
Guido van Rossum68468eb2003-02-27 20:14:51 +000092 Exception.__init__(self, *args)
Tim Peters2344fae2001-01-15 00:50:52 +000093 try:
94 self.response = args[0]
95 except IndexError:
96 self.response = 'No response given'
Barry Warsaw9dd78722000-02-10 20:25:53 +000097
98class NNTPReplyError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +000099 """Unexpected [123]xx reply"""
100 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000101
102class NNTPTemporaryError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000103 """4xx errors"""
104 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000105
106class NNTPPermanentError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000107 """5xx errors"""
108 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000109
110class NNTPProtocolError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000111 """Response does not begin with [1-5]"""
112 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000113
114class NNTPDataError(NNTPError):
Tim Peters2344fae2001-01-15 00:50:52 +0000115 """Error in response data"""
116 pass
Barry Warsaw9dd78722000-02-10 20:25:53 +0000117
Tim Peters2344fae2001-01-15 00:50:52 +0000118
Guido van Rossumc629d341992-11-05 10:43:02 +0000119# Standard port used by NNTP servers
120NNTP_PORT = 119
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000121NNTP_SSL_PORT = 563
Guido van Rossumc629d341992-11-05 10:43:02 +0000122
123# Response numbers that are followed by additional text (e.g. article)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000124_LONGRESP = {
125 '100', # HELP
126 '101', # CAPABILITIES
127 '211', # LISTGROUP (also not multi-line with GROUP)
128 '215', # LIST
129 '220', # ARTICLE
130 '221', # HEAD, XHDR
131 '222', # BODY
132 '224', # OVER, XOVER
133 '225', # HDR
134 '230', # NEWNEWS
135 '231', # NEWGROUPS
136 '282', # XGTITLE
137}
Guido van Rossumc629d341992-11-05 10:43:02 +0000138
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000139# Default decoded value for LIST OVERVIEW.FMT if not supported
140_DEFAULT_OVERVIEW_FMT = [
141 "subject", "from", "date", "message-id", "references", ":bytes", ":lines"]
142
143# Alternative names allowed in LIST OVERVIEW.FMT response
144_OVERVIEW_FMT_ALTERNATIVES = {
145 'bytes': ':bytes',
146 'lines': ':lines',
147}
Guido van Rossumc629d341992-11-05 10:43:02 +0000148
149# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000150_CRLF = b'\r\n'
151
152GroupInfo = collections.namedtuple('GroupInfo',
153 ['group', 'last', 'first', 'flag'])
154
155ArticleInfo = collections.namedtuple('ArticleInfo',
156 ['number', 'message_id', 'lines'])
Guido van Rossumc629d341992-11-05 10:43:02 +0000157
158
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000159# Helper function(s)
160def decode_header(header_str):
161 """Takes an unicode string representing a munged header value
162 and decodes it as a (possibly non-ASCII) readable value."""
163 parts = []
164 for v, enc in _email_decode_header(header_str):
165 if isinstance(v, bytes):
166 parts.append(v.decode(enc or 'ascii'))
167 else:
168 parts.append(v)
169 return ' '.join(parts)
Tim Peters2344fae2001-01-15 00:50:52 +0000170
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000171def _parse_overview_fmt(lines):
172 """Parse a list of string representing the response to LIST OVERVIEW.FMT
173 and return a list of header/metadata names.
174 Raises NNTPDataError if the response is not compliant
175 (cf. RFC 3977, section 8.4)."""
176 fmt = []
177 for line in lines:
178 if line[0] == ':':
179 # Metadata name (e.g. ":bytes")
180 name, _, suffix = line[1:].partition(':')
181 name = ':' + name
182 else:
183 # Header name (e.g. "Subject:" or "Xref:full")
184 name, _, suffix = line.partition(':')
185 name = name.lower()
186 name = _OVERVIEW_FMT_ALTERNATIVES.get(name, name)
187 # Should we do something with the suffix?
188 fmt.append(name)
189 defaults = _DEFAULT_OVERVIEW_FMT
190 if len(fmt) < len(defaults):
191 raise NNTPDataError("LIST OVERVIEW.FMT response too short")
192 if fmt[:len(defaults)] != defaults:
193 raise NNTPDataError("LIST OVERVIEW.FMT redefines default fields")
194 return fmt
195
196def _parse_overview(lines, fmt, data_process_func=None):
197 """Parse the response to a OVER or XOVER command according to the
198 overview format `fmt`."""
199 n_defaults = len(_DEFAULT_OVERVIEW_FMT)
200 overview = []
201 for line in lines:
202 fields = {}
203 article_number, *tokens = line.split('\t')
204 article_number = int(article_number)
205 for i, token in enumerate(tokens):
206 if i >= len(fmt):
207 # XXX should we raise an error? Some servers might not
208 # support LIST OVERVIEW.FMT and still return additional
209 # headers.
210 continue
211 field_name = fmt[i]
212 is_metadata = field_name.startswith(':')
213 if i >= n_defaults and not is_metadata:
214 # Non-default header names are included in full in the response
Antoine Pitrou4103bc02010-11-03 18:18:43 +0000215 # (unless the field is totally empty)
216 h = field_name + ": "
217 if token and token[:len(h)].lower() != h:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000218 raise NNTPDataError("OVER/XOVER response doesn't include "
219 "names of additional headers")
Antoine Pitrou4103bc02010-11-03 18:18:43 +0000220 token = token[len(h):] if token else None
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000221 fields[fmt[i]] = token
222 overview.append((article_number, fields))
223 return overview
224
225def _parse_datetime(date_str, time_str=None):
226 """Parse a pair of (date, time) strings, and return a datetime object.
227 If only the date is given, it is assumed to be date and time
228 concatenated together (e.g. response to the DATE command).
229 """
230 if time_str is None:
231 time_str = date_str[-6:]
232 date_str = date_str[:-6]
233 hours = int(time_str[:2])
234 minutes = int(time_str[2:4])
235 seconds = int(time_str[4:])
236 year = int(date_str[:-4])
237 month = int(date_str[-4:-2])
238 day = int(date_str[-2:])
239 # RFC 3977 doesn't say how to interpret 2-char years. Assume that
240 # there are no dates before 1970 on Usenet.
241 if year < 70:
242 year += 2000
243 elif year < 100:
244 year += 1900
245 return datetime.datetime(year, month, day, hours, minutes, seconds)
246
247def _unparse_datetime(dt, legacy=False):
248 """Format a date or datetime object as a pair of (date, time) strings
249 in the format required by the NEWNEWS and NEWGROUPS commands. If a
250 date object is passed, the time is assumed to be midnight (00h00).
251
252 The returned representation depends on the legacy flag:
253 * if legacy is False (the default):
254 date has the YYYYMMDD format and time the HHMMSS format
255 * if legacy is True:
256 date has the YYMMDD format and time the HHMMSS format.
257 RFC 3977 compliant servers should understand both formats; therefore,
258 legacy is only needed when talking to old servers.
259 """
260 if not isinstance(dt, datetime.datetime):
261 time_str = "000000"
262 else:
263 time_str = "{0.hour:02d}{0.minute:02d}{0.second:02d}".format(dt)
264 y = dt.year
265 if legacy:
266 y = y % 100
267 date_str = "{0:02d}{1.month:02d}{1.day:02d}".format(y, dt)
268 else:
269 date_str = "{0:04d}{1.month:02d}{1.day:02d}".format(y, dt)
270 return date_str, time_str
271
272
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000273if _have_ssl:
274
275 def _encrypt_on(sock, context):
276 """Wrap a socket in SSL/TLS. Arguments:
277 - sock: Socket to wrap
278 - context: SSL context to use for the encrypted connection
279 Returns:
280 - sock: New, encrypted socket.
281 """
282 # Generate a default SSL context if none was passed.
283 if context is None:
284 context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
285 # SSLv2 considered harmful.
286 context.options |= ssl.OP_NO_SSLv2
287 return context.wrap_socket(sock)
288
289
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000290# The classes themselves
291class _NNTPBase:
292 # UTF-8 is the character set for all NNTP commands and responses: they
293 # are automatically encoded (when sending) and decoded (and receiving)
294 # by this class.
295 # However, some multi-line data blocks can contain arbitrary bytes (for
296 # example, latin-1 or utf-16 data in the body of a message). Commands
297 # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
298 # data will therefore only accept and produce bytes objects.
299 # Furthermore, since there could be non-compliant servers out there,
300 # we use 'surrogateescape' as the error handler for fault tolerance
301 # and easy round-tripping. This could be useful for some applications
302 # (e.g. NNTP gateways).
303
304 encoding = 'utf-8'
305 errors = 'surrogateescape'
306
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000307 def __init__(self, file, host,
308 readermode=None, timeout=_GLOBAL_DEFAULT_TIMEOUT):
Tim Peters2344fae2001-01-15 00:50:52 +0000309 """Initialize an instance. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000310 - file: file-like object (open for read/write in binary mode)
Antoine Pitrou859c4ef2010-11-09 18:58:42 +0000311 - host: hostname of the server
Tim Peters2344fae2001-01-15 00:50:52 +0000312 - readermode: if true, send 'mode reader' command after
313 connecting.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000314 - timeout: timeout (in seconds) used for socket connections
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000315
Tim Peters2344fae2001-01-15 00:50:52 +0000316 readermode is sometimes necessary if you are connecting to an
317 NNTP server on the local machine and intend to call
Ezio Melotti42da6632011-03-15 05:18:48 +0200318 reader-specific commands, such as `group'. If you get
Tim Peters2344fae2001-01-15 00:50:52 +0000319 unexpected NNTPPermanentErrors, you might need to set
320 readermode.
321 """
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000322 self.host = host
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000323 self.file = file
Tim Peters2344fae2001-01-15 00:50:52 +0000324 self.debugging = 0
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000325 self.welcome = self._getresp()
Tim Petersdfb673b2001-01-16 07:12:46 +0000326
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000327 # 'MODE READER' is sometimes necessary to enable 'reader' mode.
328 # However, the order in which 'MODE READER' and 'AUTHINFO' need to
329 # arrive differs between some NNTP servers. If _setreadermode() fails
330 # with an authorization failed error, it will set this to True;
331 # the login() routine will interpret that as a request to try again
332 # after performing its normal function.
333 self.readermode_afterauth = False
Tim Peters2344fae2001-01-15 00:50:52 +0000334 if readermode:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000335 self._setreadermode()
Tim Petersdfb673b2001-01-16 07:12:46 +0000336
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000337 # RFC 4642 2.2.2: Both the client and the server MUST know if there is
338 # a TLS session active. A client MUST NOT attempt to start a TLS
339 # session if a TLS session is already active.
340 self.tls_on = False
341
342 # Inquire about capabilities (RFC 3977).
343 self._caps = None
344 self.getcapabilities()
345
346 # Log in and encryption setup order is left to subclasses.
347 self.authenticated = False
Guido van Rossumc629d341992-11-05 10:43:02 +0000348
Giampaolo Rodolà424298a2011-03-03 18:34:06 +0000349 def __enter__(self):
350 return self
351
352 def __exit__(self, *args):
353 is_connected = lambda: hasattr(self, "file")
354 if is_connected():
355 try:
356 self.quit()
357 except (socket.error, EOFError):
358 pass
359 finally:
360 if is_connected():
361 self._close()
362
Tim Peters2344fae2001-01-15 00:50:52 +0000363 def getwelcome(self):
364 """Get the welcome message from the server
365 (this is read and squirreled away by __init__()).
366 If the response code is 200, posting is allowed;
367 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000368
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000369 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000370 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000371
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000372 def getcapabilities(self):
373 """Get the server capabilities, as read by __init__().
374 If the CAPABILITIES command is not supported, an empty dict is
375 returned."""
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000376 if self._caps is None:
377 self.nntp_version = 1
378 self.nntp_implementation = None
379 try:
380 resp, caps = self.capabilities()
381 except NNTPPermanentError:
382 # Server doesn't support capabilities
383 self._caps = {}
384 else:
385 self._caps = caps
386 if 'VERSION' in caps:
387 # The server can advertise several supported versions,
388 # choose the highest.
389 self.nntp_version = max(map(int, caps['VERSION']))
390 if 'IMPLEMENTATION' in caps:
391 self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000392 return self._caps
393
Tim Peters2344fae2001-01-15 00:50:52 +0000394 def set_debuglevel(self, level):
395 """Set the debugging level. Argument 'level' means:
396 0: no debugging output (default)
397 1: print commands and responses but not body text etc.
398 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000399
Tim Peters2344fae2001-01-15 00:50:52 +0000400 self.debugging = level
401 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000402
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000403 def _putline(self, line):
404 """Internal: send one line to the server, appending CRLF.
405 The `line` must be a bytes-like object."""
406 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000407 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000408 self.file.write(line)
409 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000410
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000411 def _putcmd(self, line):
412 """Internal: send one command to the server (through _putline()).
413 The `line` must be an unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000414 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000415 line = line.encode(self.encoding, self.errors)
416 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000417
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000418 def _getline(self, strip_crlf=True):
419 """Internal: return one line from the server, stripping _CRLF.
420 Raise EOFError if the connection is closed.
421 Returns a bytes object."""
Tim Peters2344fae2001-01-15 00:50:52 +0000422 line = self.file.readline()
423 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000424 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000425 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000426 if strip_crlf:
427 if line[-2:] == _CRLF:
428 line = line[:-2]
429 elif line[-1:] in _CRLF:
430 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000431 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000432
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000433 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000434 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000435 Raise various errors if the response indicates an error.
436 Returns an unicode string."""
437 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000438 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000439 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000440 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000441 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000442 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000443 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000444 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000445 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000446 raise NNTPProtocolError(resp)
447 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000448
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000449 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000450 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000451 Raise various errors if the response indicates an error.
452
453 Returns a (response, lines) tuple where `response` is an unicode
454 string and `lines` is a list of bytes objects.
455 If `file` is a file-like object, it must be open in binary mode.
456 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000457
458 openedFile = None
459 try:
460 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000461 if isinstance(file, (str, bytes)):
462 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000463
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000464 resp = self._getresp()
465 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000466 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000467
468 lines = []
469 if file is not None:
470 # XXX lines = None instead?
471 terminators = (b'.' + _CRLF, b'.\n')
472 while 1:
473 line = self._getline(False)
474 if line in terminators:
475 break
476 if line.startswith(b'..'):
477 line = line[1:]
478 file.write(line)
479 else:
480 terminator = b'.'
481 while 1:
482 line = self._getline()
483 if line == terminator:
484 break
485 if line.startswith(b'..'):
486 line = line[1:]
487 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000488 finally:
489 # If this method created the file, then it must close it
490 if openedFile:
491 openedFile.close()
492
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000493 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000494
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000495 def _shortcmd(self, line):
496 """Internal: send a command and get the response.
497 Same return value as _getresp()."""
498 self._putcmd(line)
499 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000500
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000501 def _longcmd(self, line, file=None):
502 """Internal: send a command and get the response plus following text.
503 Same return value as _getlongresp()."""
504 self._putcmd(line)
505 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000506
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000507 def _longcmdstring(self, line, file=None):
508 """Internal: send a command and get the response plus following text.
509 Same as _longcmd() and _getlongresp(), except that the returned `lines`
510 are unicode strings rather than bytes objects.
511 """
512 self._putcmd(line)
513 resp, list = self._getlongresp(file)
514 return resp, [line.decode(self.encoding, self.errors)
515 for line in list]
516
517 def _getoverviewfmt(self):
518 """Internal: get the overview format. Queries the server if not
519 already done, else returns the cached value."""
520 try:
521 return self._cachedoverviewfmt
522 except AttributeError:
523 pass
524 try:
525 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
526 except NNTPPermanentError:
527 # Not supported by server?
528 fmt = _DEFAULT_OVERVIEW_FMT[:]
529 else:
530 fmt = _parse_overview_fmt(lines)
531 self._cachedoverviewfmt = fmt
532 return fmt
533
534 def _grouplist(self, lines):
535 # Parse lines into "group last first flag"
536 return [GroupInfo(*line.split()) for line in lines]
537
538 def capabilities(self):
539 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000540 Return:
541 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000542 - caps: a dictionary mapping capability names to lists of tokens
543 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
544 """
545 caps = {}
546 resp, lines = self._longcmdstring("CAPABILITIES")
547 for line in lines:
548 name, *tokens = line.split()
549 caps[name] = tokens
550 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000551
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000552 def newgroups(self, date, *, file=None):
553 """Process a NEWGROUPS command. Arguments:
554 - date: a date or datetime object
555 Return:
556 - resp: server response if successful
557 - list: list of newsgroup names
558 """
559 if not isinstance(date, (datetime.date, datetime.date)):
560 raise TypeError(
561 "the date parameter must be a date or datetime object, "
562 "not '{:40}'".format(date.__class__.__name__))
563 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
564 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
565 resp, lines = self._longcmdstring(cmd, file)
566 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000567
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000568 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000569 """Process a NEWNEWS command. Arguments:
570 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000571 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000572 Return:
573 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000574 - list: list of message ids
575 """
576 if not isinstance(date, (datetime.date, datetime.date)):
577 raise TypeError(
578 "the date parameter must be a date or datetime object, "
579 "not '{:40}'".format(date.__class__.__name__))
580 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
581 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
582 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000583
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000584 def list(self, group_pattern=None, *, file=None):
585 """Process a LIST or LIST ACTIVE command. Arguments:
586 - group_pattern: a pattern indicating which groups to query
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000587 - file: Filename string or file object to store the result in
588 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000589 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000590 - list: list of (group, last, first, flag) (strings)
591 """
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000592 if group_pattern is not None:
593 command = 'LIST ACTIVE ' + group_pattern
594 else:
595 command = 'LIST'
596 resp, lines = self._longcmdstring(command, file)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000597 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000598
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000599 def _getdescriptions(self, group_pattern, return_all):
600 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
601 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
602 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
603 if not resp.startswith('215'):
604 # Now the deprecated XGTITLE. This either raises an error
605 # or succeeds with the same output structure as LIST
606 # NEWSGROUPS.
607 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
608 groups = {}
609 for raw_line in lines:
610 match = line_pat.search(raw_line.strip())
611 if match:
612 name, desc = match.group(1, 2)
613 if not return_all:
614 return desc
615 groups[name] = desc
616 if return_all:
617 return resp, groups
618 else:
619 # Nothing found
620 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000621
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000622 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000623 """Get a description for a single group. If more than one
624 group matches ('group' is a pattern), return the first. If no
625 group matches, return an empty string.
626
627 This elides the response code from the server, since it can
628 only be '215' or '285' (for xgtitle) anyway. If the response
629 code is needed, use the 'descriptions' method.
630
631 NOTE: This neither checks for a wildcard in 'group' nor does
632 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000633 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000634
635 def descriptions(self, group_pattern):
636 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000637 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000638
Tim Peters2344fae2001-01-15 00:50:52 +0000639 def group(self, name):
640 """Process a GROUP command. Argument:
641 - group: the group name
642 Returns:
643 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000644 - count: number of articles
645 - first: first article number
646 - last: last article number
647 - name: the group name
648 """
649 resp = self._shortcmd('GROUP ' + name)
650 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000651 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000652 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000653 count = first = last = 0
654 n = len(words)
655 if n > 1:
656 count = words[1]
657 if n > 2:
658 first = words[2]
659 if n > 3:
660 last = words[3]
661 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000662 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000663 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000664
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000665 def help(self, *, file=None):
666 """Process a HELP command. Argument:
667 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000668 Returns:
669 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000670 - list: list of strings returned by the server in response to the
671 HELP command
672 """
673 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000674
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000675 def _statparse(self, resp):
676 """Internal: parse the response line of a STAT, NEXT, LAST,
677 ARTICLE, HEAD or BODY command."""
678 if not resp.startswith('22'):
679 raise NNTPReplyError(resp)
680 words = resp.split()
681 art_num = int(words[1])
682 message_id = words[2]
683 return resp, art_num, message_id
684
685 def _statcmd(self, line):
686 """Internal: process a STAT, NEXT or LAST command."""
687 resp = self._shortcmd(line)
688 return self._statparse(resp)
689
690 def stat(self, message_spec=None):
691 """Process a STAT command. Argument:
692 - message_spec: article number or message id (if not specified,
693 the current article is selected)
694 Returns:
695 - resp: server response if successful
696 - art_num: the article number
697 - message_id: the message id
698 """
699 if message_spec:
700 return self._statcmd('STAT {0}'.format(message_spec))
701 else:
702 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000703
Tim Peters2344fae2001-01-15 00:50:52 +0000704 def next(self):
705 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000706 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000707
Tim Peters2344fae2001-01-15 00:50:52 +0000708 def last(self):
709 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000710 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000711
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000712 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000713 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000714 resp, lines = self._longcmd(line, file)
715 resp, art_num, message_id = self._statparse(resp)
716 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000717
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000718 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000719 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000720 - message_spec: article number or message id
721 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000722 Returns:
723 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000724 - ArticleInfo: (article number, message id, list of header lines)
725 """
726 if message_spec is not None:
727 cmd = 'HEAD {0}'.format(message_spec)
728 else:
729 cmd = 'HEAD'
730 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000731
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000732 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000733 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000734 - message_spec: article number or message id
735 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000736 Returns:
737 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000738 - ArticleInfo: (article number, message id, list of body lines)
739 """
740 if message_spec is not None:
741 cmd = 'BODY {0}'.format(message_spec)
742 else:
743 cmd = 'BODY'
744 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000745
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000746 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000747 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000748 - message_spec: article number or message id
749 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000750 Returns:
751 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000752 - ArticleInfo: (article number, message id, list of article lines)
753 """
754 if message_spec is not None:
755 cmd = 'ARTICLE {0}'.format(message_spec)
756 else:
757 cmd = 'ARTICLE'
758 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000759
Tim Peters2344fae2001-01-15 00:50:52 +0000760 def slave(self):
761 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000762 - resp: server response if successful
763 """
764 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000765
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000766 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000767 """Process an XHDR command (optional server extension). Arguments:
768 - hdr: the header type (e.g. 'subject')
769 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000770 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000771 Returns:
772 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000773 - list: list of (nr, value) strings
774 """
775 pat = re.compile('^([0-9]+) ?(.*)\n?')
776 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
777 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000778 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000779 return m.group(1, 2) if m else line
780 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000781
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000782 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000783 """Process an XOVER command (optional server extension) Arguments:
784 - start: start of range
785 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000786 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000787 Returns:
788 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000789 - list: list of dicts containing the response fields
790 """
791 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
792 file)
793 fmt = self._getoverviewfmt()
794 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000795
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000796 def over(self, message_spec, *, file=None):
797 """Process an OVER command. If the command isn't supported, fall
798 back to XOVER. Arguments:
799 - message_spec:
800 - either a message id, indicating the article to fetch
801 information about
802 - or a (start, end) tuple, indicating a range of article numbers;
803 if end is None, information up to the newest message will be
804 retrieved
805 - or None, indicating the current article number must be used
806 - file: Filename string or file object to store the result in
807 Returns:
808 - resp: server response if successful
809 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000810
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000811 NOTE: the "message id" form isn't supported by XOVER
812 """
813 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
814 if isinstance(message_spec, (tuple, list)):
815 start, end = message_spec
816 cmd += ' {0}-{1}'.format(start, end or '')
817 elif message_spec is not None:
818 cmd = cmd + ' ' + message_spec
819 resp, lines = self._longcmdstring(cmd, file)
820 fmt = self._getoverviewfmt()
821 return resp, _parse_overview(lines, fmt)
822
823 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000824 """Process an XGTITLE command (optional server extension) Arguments:
825 - group: group name wildcard (i.e. news.*)
826 Returns:
827 - resp: server response if successful
828 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000829 warnings.warn("The XGTITLE extension is not actively used, "
830 "use descriptions() instead",
831 PendingDeprecationWarning, 2)
832 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
833 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000834 lines = []
835 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000836 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000837 if match:
838 lines.append(match.group(1, 2))
839 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000840
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000841 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000842 """Process an XPATH command (optional server extension) Arguments:
843 - id: Message id of article
844 Returns:
845 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000846 path: directory path to article
847 """
848 warnings.warn("The XPATH extension is not actively used",
849 PendingDeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000850
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000851 resp = self._shortcmd('XPATH {0}'.format(id))
852 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000853 raise NNTPReplyError(resp)
854 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000855 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000856 except ValueError:
857 raise NNTPReplyError(resp)
858 else:
859 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000860
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000861 def date(self):
862 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000863 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000864 - resp: server response if successful
865 - date: datetime object
866 """
867 resp = self._shortcmd("DATE")
868 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000869 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000870 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000871 if len(elem) != 2:
872 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000873 date = elem[1]
874 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000875 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000876 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000877
Christian Heimes933238a2008-11-05 19:44:21 +0000878 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000879 resp = self._shortcmd(command)
880 # Raises a specific exception if posting is not allowed
881 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000882 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000883 if isinstance(f, (bytes, bytearray)):
884 f = f.splitlines()
885 # We don't use _putline() because:
886 # - we don't want additional CRLF if the file or iterable is already
887 # in the right format
888 # - we don't want a spurious flush() after each line is written
889 for line in f:
890 if not line.endswith(_CRLF):
891 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000892 if line.startswith(b'.'):
893 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000894 self.file.write(line)
895 self.file.write(b".\r\n")
896 self.file.flush()
897 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000898
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000899 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000900 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000901 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000902 Returns:
903 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000904 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000905
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000906 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000907 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000908 - message_id: message-id of the article
909 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000910 Returns:
911 - resp: server response if successful
912 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000913 return self._post('IHAVE {0}'.format(message_id), data)
914
915 def _close(self):
916 self.file.close()
917 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000918
Tim Peters2344fae2001-01-15 00:50:52 +0000919 def quit(self):
920 """Process a QUIT command and close the socket. Returns:
921 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000922 try:
923 resp = self._shortcmd('QUIT')
924 finally:
925 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000926 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000927
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000928 def login(self, user=None, password=None, usenetrc=True):
929 if self.authenticated:
930 raise ValueError("Already logged in.")
931 if not user and not usenetrc:
932 raise ValueError(
933 "At least one of `user` and `usenetrc` must be specified")
934 # If no login/password was specified but netrc was requested,
935 # try to get them from ~/.netrc
936 # Presume that if .netrc has an entry, NNRP authentication is required.
937 try:
938 if usenetrc and not user:
939 import netrc
940 credentials = netrc.netrc()
941 auth = credentials.authenticators(self.host)
942 if auth:
943 user = auth[0]
944 password = auth[2]
945 except IOError:
946 pass
947 # Perform NNTP authentication if needed.
948 if not user:
949 return
950 resp = self._shortcmd('authinfo user ' + user)
951 if resp.startswith('381'):
952 if not password:
953 raise NNTPReplyError(resp)
954 else:
955 resp = self._shortcmd('authinfo pass ' + password)
956 if not resp.startswith('281'):
957 raise NNTPPermanentError(resp)
958 # Attempt to send mode reader if it was requested after login.
959 if self.readermode_afterauth:
960 self._setreadermode()
961
962 def _setreadermode(self):
963 try:
964 self.welcome = self._shortcmd('mode reader')
965 except NNTPPermanentError:
966 # Error 5xx, probably 'not implemented'
967 pass
968 except NNTPTemporaryError as e:
969 if e.response.startswith('480'):
970 # Need authorization before 'mode reader'
971 self.readermode_afterauth = True
972 else:
973 raise
974
975 if _have_ssl:
976 def starttls(self, context=None):
977 """Process a STARTTLS command. Arguments:
978 - context: SSL context to use for the encrypted connection
979 """
980 # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if
981 # a TLS session already exists.
982 if self.tls_on:
983 raise ValueError("TLS is already enabled.")
984 if self.authenticated:
985 raise ValueError("TLS cannot be started after authentication.")
986 resp = self._shortcmd('STARTTLS')
987 if resp.startswith('382'):
988 self.file.close()
989 self.sock = _encrypt_on(self.sock, context)
990 self.file = self.sock.makefile("rwb")
991 self.tls_on = True
992 # Capabilities may change after TLS starts up, so ask for them
993 # again.
994 self._caps = None
995 self.getcapabilities()
996 else:
997 raise NNTPError("TLS failed to start.")
998
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000999
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001000class NNTP(_NNTPBase):
1001
1002 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001003 readermode=None, usenetrc=False,
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001004 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1005 """Initialize an instance. Arguments:
1006 - host: hostname to connect to
1007 - port: port to connect to (default the standard NNTP port)
1008 - user: username to authenticate with
1009 - password: password to use with username
1010 - readermode: if true, send 'mode reader' command after
1011 connecting.
1012 - usenetrc: allow loading username and password from ~/.netrc file
1013 if not specified explicitly
1014 - timeout: timeout (in seconds) used for socket connections
1015
1016 readermode is sometimes necessary if you are connecting to an
1017 NNTP server on the local machine and intend to call
Ezio Melotti4969f702011-03-15 05:59:46 +02001018 reader-specific commands, such as `group'. If you get
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001019 unexpected NNTPPermanentErrors, you might need to set
1020 readermode.
1021 """
1022 self.host = host
1023 self.port = port
1024 self.sock = socket.create_connection((host, port), timeout)
1025 file = self.sock.makefile("rwb")
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001026 _NNTPBase.__init__(self, file, host,
1027 readermode, timeout)
1028 if user or usenetrc:
1029 self.login(user, password, usenetrc)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001030
1031 def _close(self):
1032 try:
1033 _NNTPBase._close(self)
1034 finally:
1035 self.sock.close()
1036
1037
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001038if _have_ssl:
1039 class NNTP_SSL(_NNTPBase):
1040
1041 def __init__(self, host, port=NNTP_SSL_PORT,
1042 user=None, password=None, ssl_context=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001043 readermode=None, usenetrc=False,
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001044 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1045 """This works identically to NNTP.__init__, except for the change
1046 in default port and the `ssl_context` argument for SSL connections.
1047 """
1048 self.sock = socket.create_connection((host, port), timeout)
1049 self.sock = _encrypt_on(self.sock, ssl_context)
1050 file = self.sock.makefile("rwb")
1051 _NNTPBase.__init__(self, file, host,
1052 readermode=readermode, timeout=timeout)
1053 if user or usenetrc:
1054 self.login(user, password, usenetrc)
1055
1056 def _close(self):
1057 try:
1058 _NNTPBase._close(self)
1059 finally:
1060 self.sock.close()
1061
1062 __all__.append("NNTP_SSL")
1063
1064
Neal Norwitzef679562002-11-14 02:19:44 +00001065# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +00001066if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001067 import argparse
1068 from email.utils import parsedate
1069
1070 parser = argparse.ArgumentParser(description="""\
1071 nntplib built-in demo - display the latest articles in a newsgroup""")
1072 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
1073 help='group to fetch messages from (default: %(default)s)')
1074 parser.add_argument('-s', '--server', default='news.gmane.org',
1075 help='NNTP server hostname (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001076 parser.add_argument('-p', '--port', default=-1, type=int,
1077 help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT))
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001078 parser.add_argument('-n', '--nb-articles', default=10, type=int,
1079 help='number of articles to fetch (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001080 parser.add_argument('-S', '--ssl', action='store_true', default=False,
1081 help='use NNTP over SSL')
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001082 args = parser.parse_args()
1083
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001084 port = args.port
1085 if not args.ssl:
1086 if port == -1:
1087 port = NNTP_PORT
1088 s = NNTP(host=args.server, port=port)
1089 else:
1090 if port == -1:
1091 port = NNTP_SSL_PORT
1092 s = NNTP_SSL(host=args.server, port=port)
1093
1094 caps = s.getcapabilities()
1095 if 'STARTTLS' in caps:
1096 s.starttls()
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001097 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001098 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001099
1100 def cut(s, lim):
1101 if len(s) > lim:
1102 s = s[:lim - 4] + "..."
1103 return s
1104
1105 first = str(int(last) - args.nb_articles + 1)
1106 resp, overviews = s.xover(first, last)
1107 for artnum, over in overviews:
1108 author = decode_header(over['from']).split('<', 1)[0]
1109 subject = decode_header(over['subject'])
1110 lines = int(over[':lines'])
1111 print("{:7} {:20} {:42} ({})".format(
1112 artnum, cut(author, 20), cut(subject, 42), lines)
1113 )
1114
1115 s.quit()