blob: 32bffd8e27c591a2a8e88b4b919fc8c259e09407 [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 Pitrou71135622012-02-14 23:29:34 +0100327 # Inquire about capabilities (RFC 3977).
328 self._caps = None
329 self.getcapabilities()
330
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000331 # 'MODE READER' is sometimes necessary to enable 'reader' mode.
332 # However, the order in which 'MODE READER' and 'AUTHINFO' need to
333 # arrive differs between some NNTP servers. If _setreadermode() fails
334 # with an authorization failed error, it will set this to True;
335 # the login() routine will interpret that as a request to try again
336 # after performing its normal function.
Antoine Pitrou71135622012-02-14 23:29:34 +0100337 # Enable only if we're not already in READER mode anyway.
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000338 self.readermode_afterauth = False
Antoine Pitrou71135622012-02-14 23:29:34 +0100339 if readermode and 'READER' not in self._caps:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000340 self._setreadermode()
Antoine Pitrou71135622012-02-14 23:29:34 +0100341 if not self.readermode_afterauth:
342 # Capabilities might have changed after MODE READER
343 self._caps = None
344 self.getcapabilities()
Tim Petersdfb673b2001-01-16 07:12:46 +0000345
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000346 # RFC 4642 2.2.2: Both the client and the server MUST know if there is
347 # a TLS session active. A client MUST NOT attempt to start a TLS
348 # session if a TLS session is already active.
349 self.tls_on = False
350
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000351 # Log in and encryption setup order is left to subclasses.
352 self.authenticated = False
Guido van Rossumc629d341992-11-05 10:43:02 +0000353
Tim Peters2344fae2001-01-15 00:50:52 +0000354 def getwelcome(self):
355 """Get the welcome message from the server
356 (this is read and squirreled away by __init__()).
357 If the response code is 200, posting is allowed;
358 if it 201, posting is not allowed."""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000359
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000360 if self.debugging: print('*welcome*', repr(self.welcome))
Tim Peters2344fae2001-01-15 00:50:52 +0000361 return self.welcome
Guido van Rossumc629d341992-11-05 10:43:02 +0000362
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000363 def getcapabilities(self):
364 """Get the server capabilities, as read by __init__().
365 If the CAPABILITIES command is not supported, an empty dict is
366 returned."""
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000367 if self._caps is None:
368 self.nntp_version = 1
369 self.nntp_implementation = None
370 try:
371 resp, caps = self.capabilities()
Antoine Pitrou54411c12012-02-12 19:14:17 +0100372 except (NNTPPermanentError, NNTPTemporaryError):
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000373 # Server doesn't support capabilities
374 self._caps = {}
375 else:
376 self._caps = caps
377 if 'VERSION' in caps:
378 # The server can advertise several supported versions,
379 # choose the highest.
380 self.nntp_version = max(map(int, caps['VERSION']))
381 if 'IMPLEMENTATION' in caps:
382 self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000383 return self._caps
384
Tim Peters2344fae2001-01-15 00:50:52 +0000385 def set_debuglevel(self, level):
386 """Set the debugging level. Argument 'level' means:
387 0: no debugging output (default)
388 1: print commands and responses but not body text etc.
389 2: also print raw lines read and sent before stripping CR/LF"""
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000390
Tim Peters2344fae2001-01-15 00:50:52 +0000391 self.debugging = level
392 debug = set_debuglevel
Guido van Rossumc629d341992-11-05 10:43:02 +0000393
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000394 def _putline(self, line):
395 """Internal: send one line to the server, appending CRLF.
396 The `line` must be a bytes-like object."""
397 line = line + _CRLF
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000398 if self.debugging > 1: print('*put*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000399 self.file.write(line)
400 self.file.flush()
Guido van Rossumc629d341992-11-05 10:43:02 +0000401
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000402 def _putcmd(self, line):
403 """Internal: send one command to the server (through _putline()).
404 The `line` must be an unicode string."""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000405 if self.debugging: print('*cmd*', repr(line))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000406 line = line.encode(self.encoding, self.errors)
407 self._putline(line)
Guido van Rossumc629d341992-11-05 10:43:02 +0000408
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000409 def _getline(self, strip_crlf=True):
410 """Internal: return one line from the server, stripping _CRLF.
411 Raise EOFError if the connection is closed.
412 Returns a bytes object."""
Tim Peters2344fae2001-01-15 00:50:52 +0000413 line = self.file.readline()
414 if self.debugging > 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000415 print('*get*', repr(line))
Tim Peters2344fae2001-01-15 00:50:52 +0000416 if not line: raise EOFError
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000417 if strip_crlf:
418 if line[-2:] == _CRLF:
419 line = line[:-2]
420 elif line[-1:] in _CRLF:
421 line = line[:-1]
Tim Peters2344fae2001-01-15 00:50:52 +0000422 return line
Guido van Rossumc629d341992-11-05 10:43:02 +0000423
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000424 def _getresp(self):
Tim Peters2344fae2001-01-15 00:50:52 +0000425 """Internal: get a response from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000426 Raise various errors if the response indicates an error.
427 Returns an unicode string."""
428 resp = self._getline()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000429 if self.debugging: print('*resp*', repr(resp))
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000430 resp = resp.decode(self.encoding, self.errors)
Tim Peters2344fae2001-01-15 00:50:52 +0000431 c = resp[:1]
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000432 if c == '4':
Tim Peters2344fae2001-01-15 00:50:52 +0000433 raise NNTPTemporaryError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000434 if c == '5':
Tim Peters2344fae2001-01-15 00:50:52 +0000435 raise NNTPPermanentError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000436 if c not in '123':
Tim Peters2344fae2001-01-15 00:50:52 +0000437 raise NNTPProtocolError(resp)
438 return resp
Guido van Rossumc629d341992-11-05 10:43:02 +0000439
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000440 def _getlongresp(self, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000441 """Internal: get a response plus following text from the server.
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000442 Raise various errors if the response indicates an error.
443
444 Returns a (response, lines) tuple where `response` is an unicode
445 string and `lines` is a list of bytes objects.
446 If `file` is a file-like object, it must be open in binary mode.
447 """
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000448
449 openedFile = None
450 try:
451 # If a string was passed then open a file with that name
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000452 if isinstance(file, (str, bytes)):
453 openedFile = file = open(file, "wb")
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000454
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000455 resp = self._getresp()
456 if resp[:3] not in _LONGRESP:
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000457 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000458
459 lines = []
460 if file is not None:
461 # XXX lines = None instead?
462 terminators = (b'.' + _CRLF, b'.\n')
463 while 1:
464 line = self._getline(False)
465 if line in terminators:
466 break
467 if line.startswith(b'..'):
468 line = line[1:]
469 file.write(line)
470 else:
471 terminator = b'.'
472 while 1:
473 line = self._getline()
474 if line == terminator:
475 break
476 if line.startswith(b'..'):
477 line = line[1:]
478 lines.append(line)
Guido van Rossumd1d584f2001-10-01 13:46:55 +0000479 finally:
480 # If this method created the file, then it must close it
481 if openedFile:
482 openedFile.close()
483
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000484 return resp, lines
Guido van Rossumc629d341992-11-05 10:43:02 +0000485
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000486 def _shortcmd(self, line):
487 """Internal: send a command and get the response.
488 Same return value as _getresp()."""
489 self._putcmd(line)
490 return self._getresp()
Guido van Rossumc629d341992-11-05 10:43:02 +0000491
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000492 def _longcmd(self, line, file=None):
493 """Internal: send a command and get the response plus following text.
494 Same return value as _getlongresp()."""
495 self._putcmd(line)
496 return self._getlongresp(file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000497
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000498 def _longcmdstring(self, line, file=None):
499 """Internal: send a command and get the response plus following text.
500 Same as _longcmd() and _getlongresp(), except that the returned `lines`
501 are unicode strings rather than bytes objects.
502 """
503 self._putcmd(line)
504 resp, list = self._getlongresp(file)
505 return resp, [line.decode(self.encoding, self.errors)
506 for line in list]
507
508 def _getoverviewfmt(self):
509 """Internal: get the overview format. Queries the server if not
510 already done, else returns the cached value."""
511 try:
512 return self._cachedoverviewfmt
513 except AttributeError:
514 pass
515 try:
516 resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
517 except NNTPPermanentError:
518 # Not supported by server?
519 fmt = _DEFAULT_OVERVIEW_FMT[:]
520 else:
521 fmt = _parse_overview_fmt(lines)
522 self._cachedoverviewfmt = fmt
523 return fmt
524
525 def _grouplist(self, lines):
526 # Parse lines into "group last first flag"
527 return [GroupInfo(*line.split()) for line in lines]
528
529 def capabilities(self):
530 """Process a CAPABILITIES command. Not supported by all servers.
Tim Peters2344fae2001-01-15 00:50:52 +0000531 Return:
532 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000533 - caps: a dictionary mapping capability names to lists of tokens
534 (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
535 """
536 caps = {}
537 resp, lines = self._longcmdstring("CAPABILITIES")
538 for line in lines:
539 name, *tokens = line.split()
540 caps[name] = tokens
541 return resp, caps
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000542
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000543 def newgroups(self, date, *, file=None):
544 """Process a NEWGROUPS command. Arguments:
545 - date: a date or datetime object
546 Return:
547 - resp: server response if successful
548 - list: list of newsgroup names
549 """
550 if not isinstance(date, (datetime.date, datetime.date)):
551 raise TypeError(
552 "the date parameter must be a date or datetime object, "
553 "not '{:40}'".format(date.__class__.__name__))
554 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
555 cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
556 resp, lines = self._longcmdstring(cmd, file)
557 return resp, self._grouplist(lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000558
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000559 def newnews(self, group, date, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000560 """Process a NEWNEWS command. Arguments:
561 - group: group name or '*'
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000562 - date: a date or datetime object
Tim Peters2344fae2001-01-15 00:50:52 +0000563 Return:
564 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000565 - list: list of message ids
566 """
567 if not isinstance(date, (datetime.date, datetime.date)):
568 raise TypeError(
569 "the date parameter must be a date or datetime object, "
570 "not '{:40}'".format(date.__class__.__name__))
571 date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
572 cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
573 return self._longcmdstring(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000574
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000575 def list(self, group_pattern=None, *, file=None):
576 """Process a LIST or LIST ACTIVE command. Arguments:
577 - group_pattern: a pattern indicating which groups to query
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000578 - file: Filename string or file object to store the result in
579 Returns:
Tim Peters2344fae2001-01-15 00:50:52 +0000580 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000581 - list: list of (group, last, first, flag) (strings)
582 """
Antoine Pitrou08eeada2010-11-04 21:36:15 +0000583 if group_pattern is not None:
584 command = 'LIST ACTIVE ' + group_pattern
585 else:
586 command = 'LIST'
587 resp, lines = self._longcmdstring(command, file)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000588 return resp, self._grouplist(lines)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000589
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000590 def _getdescriptions(self, group_pattern, return_all):
591 line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
592 # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
593 resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
594 if not resp.startswith('215'):
595 # Now the deprecated XGTITLE. This either raises an error
596 # or succeeds with the same output structure as LIST
597 # NEWSGROUPS.
598 resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
599 groups = {}
600 for raw_line in lines:
601 match = line_pat.search(raw_line.strip())
602 if match:
603 name, desc = match.group(1, 2)
604 if not return_all:
605 return desc
606 groups[name] = desc
607 if return_all:
608 return resp, groups
609 else:
610 # Nothing found
611 return ''
Guido van Rossumc629d341992-11-05 10:43:02 +0000612
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000613 def description(self, group):
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000614 """Get a description for a single group. If more than one
615 group matches ('group' is a pattern), return the first. If no
616 group matches, return an empty string.
617
618 This elides the response code from the server, since it can
619 only be '215' or '285' (for xgtitle) anyway. If the response
620 code is needed, use the 'descriptions' method.
621
622 NOTE: This neither checks for a wildcard in 'group' nor does
623 it check whether the group actually exists."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000624 return self._getdescriptions(group, False)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000625
626 def descriptions(self, group_pattern):
627 """Get descriptions for a range of groups."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000628 return self._getdescriptions(group_pattern, True)
Martin v. Löwiscc0f9322004-07-26 12:40:50 +0000629
Tim Peters2344fae2001-01-15 00:50:52 +0000630 def group(self, name):
631 """Process a GROUP command. Argument:
632 - group: the group name
633 Returns:
634 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000635 - count: number of articles
636 - first: first article number
637 - last: last article number
638 - name: the group name
639 """
640 resp = self._shortcmd('GROUP ' + name)
641 if not resp.startswith('211'):
Tim Peters2344fae2001-01-15 00:50:52 +0000642 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000643 words = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000644 count = first = last = 0
645 n = len(words)
646 if n > 1:
647 count = words[1]
648 if n > 2:
649 first = words[2]
650 if n > 3:
651 last = words[3]
652 if n > 4:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000653 name = words[4].lower()
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000654 return resp, int(count), int(first), int(last), name
Guido van Rossumc629d341992-11-05 10:43:02 +0000655
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000656 def help(self, *, file=None):
657 """Process a HELP command. Argument:
658 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000659 Returns:
660 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000661 - list: list of strings returned by the server in response to the
662 HELP command
663 """
664 return self._longcmdstring('HELP', file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000665
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000666 def _statparse(self, resp):
667 """Internal: parse the response line of a STAT, NEXT, LAST,
668 ARTICLE, HEAD or BODY command."""
669 if not resp.startswith('22'):
670 raise NNTPReplyError(resp)
671 words = resp.split()
672 art_num = int(words[1])
673 message_id = words[2]
674 return resp, art_num, message_id
675
676 def _statcmd(self, line):
677 """Internal: process a STAT, NEXT or LAST command."""
678 resp = self._shortcmd(line)
679 return self._statparse(resp)
680
681 def stat(self, message_spec=None):
682 """Process a STAT command. Argument:
683 - message_spec: article number or message id (if not specified,
684 the current article is selected)
685 Returns:
686 - resp: server response if successful
687 - art_num: the article number
688 - message_id: the message id
689 """
690 if message_spec:
691 return self._statcmd('STAT {0}'.format(message_spec))
692 else:
693 return self._statcmd('STAT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000694
Tim Peters2344fae2001-01-15 00:50:52 +0000695 def next(self):
696 """Process a NEXT command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000697 return self._statcmd('NEXT')
Guido van Rossumc629d341992-11-05 10:43:02 +0000698
Tim Peters2344fae2001-01-15 00:50:52 +0000699 def last(self):
700 """Process a LAST command. No arguments. Return as for STAT."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000701 return self._statcmd('LAST')
Guido van Rossumc629d341992-11-05 10:43:02 +0000702
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000703 def _artcmd(self, line, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000704 """Internal: process a HEAD, BODY or ARTICLE command."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000705 resp, lines = self._longcmd(line, file)
706 resp, art_num, message_id = self._statparse(resp)
707 return resp, ArticleInfo(art_num, message_id, lines)
Guido van Rossumc629d341992-11-05 10:43:02 +0000708
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000709 def head(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000710 """Process a HEAD command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000711 - message_spec: article number or message id
712 - file: filename string or file object to store the headers in
Tim Peters2344fae2001-01-15 00:50:52 +0000713 Returns:
714 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000715 - ArticleInfo: (article number, message id, list of header lines)
716 """
717 if message_spec is not None:
718 cmd = 'HEAD {0}'.format(message_spec)
719 else:
720 cmd = 'HEAD'
721 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000722
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000723 def body(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000724 """Process a BODY command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000725 - message_spec: article number or message id
726 - file: filename string or file object to store the body in
Tim Peters2344fae2001-01-15 00:50:52 +0000727 Returns:
728 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000729 - ArticleInfo: (article number, message id, list of body lines)
730 """
731 if message_spec is not None:
732 cmd = 'BODY {0}'.format(message_spec)
733 else:
734 cmd = 'BODY'
735 return self._artcmd(cmd, file)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000736
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000737 def article(self, message_spec=None, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000738 """Process an ARTICLE command. Argument:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000739 - message_spec: article number or message id
740 - file: filename string or file object to store the article in
Tim Peters2344fae2001-01-15 00:50:52 +0000741 Returns:
742 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000743 - ArticleInfo: (article number, message id, list of article lines)
744 """
745 if message_spec is not None:
746 cmd = 'ARTICLE {0}'.format(message_spec)
747 else:
748 cmd = 'ARTICLE'
749 return self._artcmd(cmd, file)
Guido van Rossumc629d341992-11-05 10:43:02 +0000750
Tim Peters2344fae2001-01-15 00:50:52 +0000751 def slave(self):
752 """Process a SLAVE command. Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000753 - resp: server response if successful
754 """
755 return self._shortcmd('SLAVE')
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000756
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000757 def xhdr(self, hdr, str, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000758 """Process an XHDR command (optional server extension). Arguments:
759 - hdr: the header type (e.g. 'subject')
760 - str: an article nr, a message id, or a range nr1-nr2
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000761 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000762 Returns:
763 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000764 - list: list of (nr, value) strings
765 """
766 pat = re.compile('^([0-9]+) ?(.*)\n?')
767 resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
768 def remove_number(line):
Tim Peters2344fae2001-01-15 00:50:52 +0000769 m = pat.match(line)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000770 return m.group(1, 2) if m else line
771 return resp, [remove_number(line) for line in lines]
Guido van Rossumc629d341992-11-05 10:43:02 +0000772
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000773 def xover(self, start, end, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000774 """Process an XOVER command (optional server extension) Arguments:
775 - start: start of range
776 - end: end of range
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000777 - file: Filename string or file object to store the result in
Tim Peters2344fae2001-01-15 00:50:52 +0000778 Returns:
779 - resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000780 - list: list of dicts containing the response fields
781 """
782 resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
783 file)
784 fmt = self._getoverviewfmt()
785 return resp, _parse_overview(lines, fmt)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000786
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000787 def over(self, message_spec, *, file=None):
788 """Process an OVER command. If the command isn't supported, fall
789 back to XOVER. Arguments:
790 - message_spec:
791 - either a message id, indicating the article to fetch
792 information about
793 - or a (start, end) tuple, indicating a range of article numbers;
794 if end is None, information up to the newest message will be
795 retrieved
796 - or None, indicating the current article number must be used
797 - file: Filename string or file object to store the result in
798 Returns:
799 - resp: server response if successful
800 - list: list of dicts containing the response fields
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000801
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000802 NOTE: the "message id" form isn't supported by XOVER
803 """
804 cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
805 if isinstance(message_spec, (tuple, list)):
806 start, end = message_spec
807 cmd += ' {0}-{1}'.format(start, end or '')
808 elif message_spec is not None:
809 cmd = cmd + ' ' + message_spec
810 resp, lines = self._longcmdstring(cmd, file)
811 fmt = self._getoverviewfmt()
812 return resp, _parse_overview(lines, fmt)
813
814 def xgtitle(self, group, *, file=None):
Tim Peters2344fae2001-01-15 00:50:52 +0000815 """Process an XGTITLE command (optional server extension) Arguments:
816 - group: group name wildcard (i.e. news.*)
817 Returns:
818 - resp: server response if successful
819 - list: list of (name,title) strings"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000820 warnings.warn("The XGTITLE extension is not actively used, "
821 "use descriptions() instead",
822 PendingDeprecationWarning, 2)
823 line_pat = re.compile('^([^ \t]+)[ \t]+(.*)$')
824 resp, raw_lines = self._longcmdstring('XGTITLE ' + group, file)
Tim Peters2344fae2001-01-15 00:50:52 +0000825 lines = []
826 for raw_line in raw_lines:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000827 match = line_pat.search(raw_line.strip())
Tim Peters2344fae2001-01-15 00:50:52 +0000828 if match:
829 lines.append(match.group(1, 2))
830 return resp, lines
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000831
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000832 def xpath(self, id):
Tim Peters2344fae2001-01-15 00:50:52 +0000833 """Process an XPATH command (optional server extension) Arguments:
834 - id: Message id of article
835 Returns:
836 resp: server response if successful
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000837 path: directory path to article
838 """
839 warnings.warn("The XPATH extension is not actively used",
840 PendingDeprecationWarning, 2)
Guido van Rossum54f22ed2000-02-04 15:10:34 +0000841
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000842 resp = self._shortcmd('XPATH {0}'.format(id))
843 if not resp.startswith('223'):
Tim Peters2344fae2001-01-15 00:50:52 +0000844 raise NNTPReplyError(resp)
845 try:
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000846 [resp_num, path] = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000847 except ValueError:
848 raise NNTPReplyError(resp)
849 else:
850 return resp, path
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000851
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000852 def date(self):
853 """Process the DATE command.
Tim Peters2344fae2001-01-15 00:50:52 +0000854 Returns:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000855 - resp: server response if successful
856 - date: datetime object
857 """
858 resp = self._shortcmd("DATE")
859 if not resp.startswith('111'):
Tim Peters2344fae2001-01-15 00:50:52 +0000860 raise NNTPReplyError(resp)
Eric S. Raymondb9c24fb2001-02-09 07:02:17 +0000861 elem = resp.split()
Tim Peters2344fae2001-01-15 00:50:52 +0000862 if len(elem) != 2:
863 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000864 date = elem[1]
865 if len(date) != 14:
Tim Peters2344fae2001-01-15 00:50:52 +0000866 raise NNTPDataError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000867 return resp, _parse_datetime(date, None)
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000868
Christian Heimes933238a2008-11-05 19:44:21 +0000869 def _post(self, command, f):
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000870 resp = self._shortcmd(command)
871 # Raises a specific exception if posting is not allowed
872 if not resp.startswith('3'):
Christian Heimes933238a2008-11-05 19:44:21 +0000873 raise NNTPReplyError(resp)
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000874 if isinstance(f, (bytes, bytearray)):
875 f = f.splitlines()
876 # We don't use _putline() because:
877 # - we don't want additional CRLF if the file or iterable is already
878 # in the right format
879 # - we don't want a spurious flush() after each line is written
880 for line in f:
881 if not line.endswith(_CRLF):
882 line = line.rstrip(b"\r\n") + _CRLF
Christian Heimes933238a2008-11-05 19:44:21 +0000883 if line.startswith(b'.'):
884 line = b'.' + line
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000885 self.file.write(line)
886 self.file.write(b".\r\n")
887 self.file.flush()
888 return self._getresp()
Guido van Rossum8421c4e1995-09-22 00:52:38 +0000889
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000890 def post(self, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000891 """Process a POST command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000892 - data: bytes object, iterable or file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000893 Returns:
894 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000895 return self._post('POST', data)
Guido van Rossumc629d341992-11-05 10:43:02 +0000896
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000897 def ihave(self, message_id, data):
Tim Peters2344fae2001-01-15 00:50:52 +0000898 """Process an IHAVE command. Arguments:
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000899 - message_id: message-id of the article
900 - data: file containing the article
Tim Peters2344fae2001-01-15 00:50:52 +0000901 Returns:
902 - resp: server response if successful
903 Note that if the server refuses the article an exception is raised."""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000904 return self._post('IHAVE {0}'.format(message_id), data)
905
906 def _close(self):
907 self.file.close()
908 del self.file
Guido van Rossumc629d341992-11-05 10:43:02 +0000909
Tim Peters2344fae2001-01-15 00:50:52 +0000910 def quit(self):
911 """Process a QUIT command and close the socket. Returns:
912 - resp: server response if successful"""
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000913 try:
914 resp = self._shortcmd('QUIT')
915 finally:
916 self._close()
Tim Peters2344fae2001-01-15 00:50:52 +0000917 return resp
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000918
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000919 def login(self, user=None, password=None, usenetrc=True):
920 if self.authenticated:
921 raise ValueError("Already logged in.")
922 if not user and not usenetrc:
923 raise ValueError(
924 "At least one of `user` and `usenetrc` must be specified")
925 # If no login/password was specified but netrc was requested,
926 # try to get them from ~/.netrc
927 # Presume that if .netrc has an entry, NNRP authentication is required.
928 try:
929 if usenetrc and not user:
930 import netrc
931 credentials = netrc.netrc()
932 auth = credentials.authenticators(self.host)
933 if auth:
934 user = auth[0]
935 password = auth[2]
936 except IOError:
937 pass
938 # Perform NNTP authentication if needed.
939 if not user:
940 return
941 resp = self._shortcmd('authinfo user ' + user)
942 if resp.startswith('381'):
943 if not password:
944 raise NNTPReplyError(resp)
945 else:
946 resp = self._shortcmd('authinfo pass ' + password)
947 if not resp.startswith('281'):
948 raise NNTPPermanentError(resp)
Antoine Pitrou54411c12012-02-12 19:14:17 +0100949 # Capabilities might have changed after login
950 self._caps = None
951 self.getcapabilities()
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000952 # Attempt to send mode reader if it was requested after login.
Antoine Pitrou71135622012-02-14 23:29:34 +0100953 # Only do so if we're not in reader mode already.
954 if self.readermode_afterauth and 'READER' not in self._caps:
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000955 self._setreadermode()
Antoine Pitrou71135622012-02-14 23:29:34 +0100956 # Capabilities might have changed after MODE READER
957 self._caps = None
958 self.getcapabilities()
Antoine Pitrou1cb121e2010-11-09 18:54:37 +0000959
960 def _setreadermode(self):
961 try:
962 self.welcome = self._shortcmd('mode reader')
963 except NNTPPermanentError:
964 # Error 5xx, probably 'not implemented'
965 pass
966 except NNTPTemporaryError as e:
967 if e.response.startswith('480'):
968 # Need authorization before 'mode reader'
969 self.readermode_afterauth = True
970 else:
971 raise
972
973 if _have_ssl:
974 def starttls(self, context=None):
975 """Process a STARTTLS command. Arguments:
976 - context: SSL context to use for the encrypted connection
977 """
978 # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if
979 # a TLS session already exists.
980 if self.tls_on:
981 raise ValueError("TLS is already enabled.")
982 if self.authenticated:
983 raise ValueError("TLS cannot be started after authentication.")
984 resp = self._shortcmd('STARTTLS')
985 if resp.startswith('382'):
986 self.file.close()
987 self.sock = _encrypt_on(self.sock, context)
988 self.file = self.sock.makefile("rwb")
989 self.tls_on = True
990 # Capabilities may change after TLS starts up, so ask for them
991 # again.
992 self._caps = None
993 self.getcapabilities()
994 else:
995 raise NNTPError("TLS failed to start.")
996
Guido van Rossume2ed9df1997-08-26 23:26:18 +0000997
Antoine Pitrou69ab9512010-09-29 15:03:40 +0000998class NNTP(_NNTPBase):
999
1000 def __init__(self, host, port=NNTP_PORT, user=None, password=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001001 readermode=None, usenetrc=False,
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001002 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1003 """Initialize an instance. Arguments:
1004 - host: hostname to connect to
1005 - port: port to connect to (default the standard NNTP port)
1006 - user: username to authenticate with
1007 - password: password to use with username
1008 - readermode: if true, send 'mode reader' command after
1009 connecting.
1010 - usenetrc: allow loading username and password from ~/.netrc file
1011 if not specified explicitly
1012 - timeout: timeout (in seconds) used for socket connections
1013
1014 readermode is sometimes necessary if you are connecting to an
1015 NNTP server on the local machine and intend to call
Ezio Melotti4969f702011-03-15 05:59:46 +02001016 reader-specific commands, such as `group'. If you get
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001017 unexpected NNTPPermanentErrors, you might need to set
1018 readermode.
1019 """
1020 self.host = host
1021 self.port = port
1022 self.sock = socket.create_connection((host, port), timeout)
1023 file = self.sock.makefile("rwb")
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001024 _NNTPBase.__init__(self, file, host,
1025 readermode, timeout)
1026 if user or usenetrc:
1027 self.login(user, password, usenetrc)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001028
1029 def _close(self):
1030 try:
1031 _NNTPBase._close(self)
1032 finally:
1033 self.sock.close()
1034
1035
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001036if _have_ssl:
1037 class NNTP_SSL(_NNTPBase):
1038
1039 def __init__(self, host, port=NNTP_SSL_PORT,
1040 user=None, password=None, ssl_context=None,
Antoine Pitrou859c4ef2010-11-09 18:58:42 +00001041 readermode=None, usenetrc=False,
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001042 timeout=_GLOBAL_DEFAULT_TIMEOUT):
1043 """This works identically to NNTP.__init__, except for the change
1044 in default port and the `ssl_context` argument for SSL connections.
1045 """
1046 self.sock = socket.create_connection((host, port), timeout)
1047 self.sock = _encrypt_on(self.sock, ssl_context)
1048 file = self.sock.makefile("rwb")
1049 _NNTPBase.__init__(self, file, host,
1050 readermode=readermode, timeout=timeout)
1051 if user or usenetrc:
1052 self.login(user, password, usenetrc)
1053
1054 def _close(self):
1055 try:
1056 _NNTPBase._close(self)
1057 finally:
1058 self.sock.close()
1059
1060 __all__.append("NNTP_SSL")
1061
1062
Neal Norwitzef679562002-11-14 02:19:44 +00001063# Test retrieval when run as a script.
Eric S. Raymondb2db5872002-11-13 23:05:35 +00001064if __name__ == '__main__':
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001065 import argparse
1066 from email.utils import parsedate
1067
1068 parser = argparse.ArgumentParser(description="""\
1069 nntplib built-in demo - display the latest articles in a newsgroup""")
1070 parser.add_argument('-g', '--group', default='gmane.comp.python.general',
1071 help='group to fetch messages from (default: %(default)s)')
1072 parser.add_argument('-s', '--server', default='news.gmane.org',
1073 help='NNTP server hostname (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001074 parser.add_argument('-p', '--port', default=-1, type=int,
1075 help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT))
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001076 parser.add_argument('-n', '--nb-articles', default=10, type=int,
1077 help='number of articles to fetch (default: %(default)s)')
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001078 parser.add_argument('-S', '--ssl', action='store_true', default=False,
1079 help='use NNTP over SSL')
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001080 args = parser.parse_args()
1081
Antoine Pitrou1cb121e2010-11-09 18:54:37 +00001082 port = args.port
1083 if not args.ssl:
1084 if port == -1:
1085 port = NNTP_PORT
1086 s = NNTP(host=args.server, port=port)
1087 else:
1088 if port == -1:
1089 port = NNTP_SSL_PORT
1090 s = NNTP_SSL(host=args.server, port=port)
1091
1092 caps = s.getcapabilities()
1093 if 'STARTTLS' in caps:
1094 s.starttls()
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001095 resp, count, first, last, name = s.group(args.group)
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001096 print('Group', name, 'has', count, 'articles, range', first, 'to', last)
Antoine Pitrou69ab9512010-09-29 15:03:40 +00001097
1098 def cut(s, lim):
1099 if len(s) > lim:
1100 s = s[:lim - 4] + "..."
1101 return s
1102
1103 first = str(int(last) - args.nb_articles + 1)
1104 resp, overviews = s.xover(first, last)
1105 for artnum, over in overviews:
1106 author = decode_header(over['from']).split('<', 1)[0]
1107 subject = decode_header(over['subject'])
1108 lines = int(over[':lines'])
1109 print("{:7} {:20} {:42} ({})".format(
1110 artnum, cut(author, 20), cut(subject, 42), lines)
1111 )
1112
1113 s.quit()