aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/tools/python3/src/Lib/nntplib.py
diff options
context:
space:
mode:
authororivej <orivej@yandex-team.ru>2022-02-10 16:44:49 +0300
committerDaniil Cherednik <dcherednik@yandex-team.ru>2022-02-10 16:44:49 +0300
commit718c552901d703c502ccbefdfc3c9028d608b947 (patch)
tree46534a98bbefcd7b1f3faa5b52c138ab27db75b7 /contrib/tools/python3/src/Lib/nntplib.py
parente9656aae26e0358d5378e5b63dcac5c8dbe0e4d0 (diff)
downloadydb-718c552901d703c502ccbefdfc3c9028d608b947.tar.gz
Restoring authorship annotation for <orivej@yandex-team.ru>. Commit 1 of 2.
Diffstat (limited to 'contrib/tools/python3/src/Lib/nntplib.py')
-rw-r--r--contrib/tools/python3/src/Lib/nntplib.py2072
1 files changed, 1036 insertions, 1036 deletions
diff --git a/contrib/tools/python3/src/Lib/nntplib.py b/contrib/tools/python3/src/Lib/nntplib.py
index f6e746e7c9..cdad77b2db 100644
--- a/contrib/tools/python3/src/Lib/nntplib.py
+++ b/contrib/tools/python3/src/Lib/nntplib.py
@@ -1,335 +1,335 @@
-"""An NNTP client class based on:
-- RFC 977: Network News Transfer Protocol
-- RFC 2980: Common NNTP Extensions
-- RFC 3977: Network News Transfer Protocol (version 2)
-
-Example:
-
->>> from nntplib import NNTP
->>> s = NNTP('news')
->>> resp, count, first, last, name = s.group('comp.lang.python')
->>> print('Group', name, 'has', count, 'articles, range', first, 'to', last)
-Group comp.lang.python has 51 articles, range 5770 to 5821
->>> resp, subs = s.xhdr('subject', '{0}-{1}'.format(first, last))
->>> resp = s.quit()
->>>
-
-Here 'resp' is the server response line.
-Error responses are turned into exceptions.
-
-To post an article from a file:
->>> f = open(filename, 'rb') # file containing article, including header
->>> resp = s.post(f)
->>>
-
-For descriptions of all methods, read the comments in the code below.
-Note that all arguments and return values representing article numbers
-are strings, not numbers, since they are rarely used for calculations.
-"""
-
-# RFC 977 by Brian Kantor and Phil Lapsley.
-# xover, xgtitle, xpath, date methods by Kevan Heydon
-
-# Incompatible changes from the 2.x nntplib:
-# - all commands are encoded as UTF-8 data (using the "surrogateescape"
-# error handler), except for raw message data (POST, IHAVE)
-# - all responses are decoded as UTF-8 data (using the "surrogateescape"
-# error handler), except for raw message data (ARTICLE, HEAD, BODY)
-# - the `file` argument to various methods is keyword-only
-#
-# - NNTP.date() returns a datetime object
-# - NNTP.newgroups() and NNTP.newnews() take a datetime (or date) object,
-# rather than a pair of (date, time) strings.
-# - NNTP.newgroups() and NNTP.list() return a list of GroupInfo named tuples
-# - NNTP.descriptions() returns a dict mapping group names to descriptions
-# - NNTP.xover() returns a list of dicts mapping field names (header or metadata)
-# to field values; each dict representing a message overview.
-# - NNTP.article(), NNTP.head() and NNTP.body() return a (response, ArticleInfo)
-# tuple.
-# - the "internal" methods have been marked private (they now start with
-# an underscore)
-
-# Other changes from the 2.x/3.1 nntplib:
-# - automatic querying of capabilities at connect
-# - New method NNTP.getcapabilities()
-# - New method NNTP.over()
-# - New helper function decode_header()
-# - NNTP.post() and NNTP.ihave() accept file objects, bytes-like objects and
-# arbitrary iterables yielding lines.
-# - An extensive test suite :-)
-
-# TODO:
-# - return structured data (GroupInfo etc.) everywhere
-# - support HDR
-
-# Imports
-import re
-import socket
-import collections
-import datetime
+"""An NNTP client class based on:
+- RFC 977: Network News Transfer Protocol
+- RFC 2980: Common NNTP Extensions
+- RFC 3977: Network News Transfer Protocol (version 2)
+
+Example:
+
+>>> from nntplib import NNTP
+>>> s = NNTP('news')
+>>> resp, count, first, last, name = s.group('comp.lang.python')
+>>> print('Group', name, 'has', count, 'articles, range', first, 'to', last)
+Group comp.lang.python has 51 articles, range 5770 to 5821
+>>> resp, subs = s.xhdr('subject', '{0}-{1}'.format(first, last))
+>>> resp = s.quit()
+>>>
+
+Here 'resp' is the server response line.
+Error responses are turned into exceptions.
+
+To post an article from a file:
+>>> f = open(filename, 'rb') # file containing article, including header
+>>> resp = s.post(f)
+>>>
+
+For descriptions of all methods, read the comments in the code below.
+Note that all arguments and return values representing article numbers
+are strings, not numbers, since they are rarely used for calculations.
+"""
+
+# RFC 977 by Brian Kantor and Phil Lapsley.
+# xover, xgtitle, xpath, date methods by Kevan Heydon
+
+# Incompatible changes from the 2.x nntplib:
+# - all commands are encoded as UTF-8 data (using the "surrogateescape"
+# error handler), except for raw message data (POST, IHAVE)
+# - all responses are decoded as UTF-8 data (using the "surrogateescape"
+# error handler), except for raw message data (ARTICLE, HEAD, BODY)
+# - the `file` argument to various methods is keyword-only
+#
+# - NNTP.date() returns a datetime object
+# - NNTP.newgroups() and NNTP.newnews() take a datetime (or date) object,
+# rather than a pair of (date, time) strings.
+# - NNTP.newgroups() and NNTP.list() return a list of GroupInfo named tuples
+# - NNTP.descriptions() returns a dict mapping group names to descriptions
+# - NNTP.xover() returns a list of dicts mapping field names (header or metadata)
+# to field values; each dict representing a message overview.
+# - NNTP.article(), NNTP.head() and NNTP.body() return a (response, ArticleInfo)
+# tuple.
+# - the "internal" methods have been marked private (they now start with
+# an underscore)
+
+# Other changes from the 2.x/3.1 nntplib:
+# - automatic querying of capabilities at connect
+# - New method NNTP.getcapabilities()
+# - New method NNTP.over()
+# - New helper function decode_header()
+# - NNTP.post() and NNTP.ihave() accept file objects, bytes-like objects and
+# arbitrary iterables yielding lines.
+# - An extensive test suite :-)
+
+# TODO:
+# - return structured data (GroupInfo etc.) everywhere
+# - support HDR
+
+# Imports
+import re
+import socket
+import collections
+import datetime
import sys
-
-try:
- import ssl
-except ImportError:
- _have_ssl = False
-else:
- _have_ssl = True
-
-from email.header import decode_header as _email_decode_header
-from socket import _GLOBAL_DEFAULT_TIMEOUT
-
-__all__ = ["NNTP",
- "NNTPError", "NNTPReplyError", "NNTPTemporaryError",
- "NNTPPermanentError", "NNTPProtocolError", "NNTPDataError",
- "decode_header",
- ]
-
-# maximal line length when calling readline(). This is to prevent
-# reading arbitrary length lines. RFC 3977 limits NNTP line length to
-# 512 characters, including CRLF. We have selected 2048 just to be on
-# the safe side.
-_MAXLINE = 2048
-
-
-# Exceptions raised when an error or invalid response is received
-class NNTPError(Exception):
- """Base class for all nntplib exceptions"""
- def __init__(self, *args):
- Exception.__init__(self, *args)
- try:
- self.response = args[0]
- except IndexError:
- self.response = 'No response given'
-
-class NNTPReplyError(NNTPError):
- """Unexpected [123]xx reply"""
- pass
-
-class NNTPTemporaryError(NNTPError):
- """4xx errors"""
- pass
-
-class NNTPPermanentError(NNTPError):
- """5xx errors"""
- pass
-
-class NNTPProtocolError(NNTPError):
- """Response does not begin with [1-5]"""
- pass
-
-class NNTPDataError(NNTPError):
- """Error in response data"""
- pass
-
-
-# Standard port used by NNTP servers
-NNTP_PORT = 119
-NNTP_SSL_PORT = 563
-
-# Response numbers that are followed by additional text (e.g. article)
-_LONGRESP = {
- '100', # HELP
- '101', # CAPABILITIES
- '211', # LISTGROUP (also not multi-line with GROUP)
- '215', # LIST
- '220', # ARTICLE
- '221', # HEAD, XHDR
- '222', # BODY
- '224', # OVER, XOVER
- '225', # HDR
- '230', # NEWNEWS
- '231', # NEWGROUPS
- '282', # XGTITLE
-}
-
-# Default decoded value for LIST OVERVIEW.FMT if not supported
-_DEFAULT_OVERVIEW_FMT = [
- "subject", "from", "date", "message-id", "references", ":bytes", ":lines"]
-
-# Alternative names allowed in LIST OVERVIEW.FMT response
-_OVERVIEW_FMT_ALTERNATIVES = {
- 'bytes': ':bytes',
- 'lines': ':lines',
-}
-
-# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
-_CRLF = b'\r\n'
-
-GroupInfo = collections.namedtuple('GroupInfo',
- ['group', 'last', 'first', 'flag'])
-
-ArticleInfo = collections.namedtuple('ArticleInfo',
- ['number', 'message_id', 'lines'])
-
-
-# Helper function(s)
-def decode_header(header_str):
- """Takes a unicode string representing a munged header value
- and decodes it as a (possibly non-ASCII) readable value."""
- parts = []
- for v, enc in _email_decode_header(header_str):
- if isinstance(v, bytes):
- parts.append(v.decode(enc or 'ascii'))
- else:
- parts.append(v)
- return ''.join(parts)
-
-def _parse_overview_fmt(lines):
- """Parse a list of string representing the response to LIST OVERVIEW.FMT
- and return a list of header/metadata names.
- Raises NNTPDataError if the response is not compliant
- (cf. RFC 3977, section 8.4)."""
- fmt = []
- for line in lines:
- if line[0] == ':':
- # Metadata name (e.g. ":bytes")
- name, _, suffix = line[1:].partition(':')
- name = ':' + name
- else:
- # Header name (e.g. "Subject:" or "Xref:full")
- name, _, suffix = line.partition(':')
- name = name.lower()
- name = _OVERVIEW_FMT_ALTERNATIVES.get(name, name)
- # Should we do something with the suffix?
- fmt.append(name)
- defaults = _DEFAULT_OVERVIEW_FMT
- if len(fmt) < len(defaults):
- raise NNTPDataError("LIST OVERVIEW.FMT response too short")
- if fmt[:len(defaults)] != defaults:
- raise NNTPDataError("LIST OVERVIEW.FMT redefines default fields")
- return fmt
-
-def _parse_overview(lines, fmt, data_process_func=None):
- """Parse the response to an OVER or XOVER command according to the
- overview format `fmt`."""
- n_defaults = len(_DEFAULT_OVERVIEW_FMT)
- overview = []
- for line in lines:
- fields = {}
- article_number, *tokens = line.split('\t')
- article_number = int(article_number)
- for i, token in enumerate(tokens):
- if i >= len(fmt):
- # XXX should we raise an error? Some servers might not
- # support LIST OVERVIEW.FMT and still return additional
- # headers.
- continue
- field_name = fmt[i]
- is_metadata = field_name.startswith(':')
- if i >= n_defaults and not is_metadata:
- # Non-default header names are included in full in the response
- # (unless the field is totally empty)
- h = field_name + ": "
- if token and token[:len(h)].lower() != h:
- raise NNTPDataError("OVER/XOVER response doesn't include "
- "names of additional headers")
- token = token[len(h):] if token else None
- fields[fmt[i]] = token
- overview.append((article_number, fields))
- return overview
-
-def _parse_datetime(date_str, time_str=None):
- """Parse a pair of (date, time) strings, and return a datetime object.
- If only the date is given, it is assumed to be date and time
- concatenated together (e.g. response to the DATE command).
- """
- if time_str is None:
- time_str = date_str[-6:]
- date_str = date_str[:-6]
- hours = int(time_str[:2])
- minutes = int(time_str[2:4])
- seconds = int(time_str[4:])
- year = int(date_str[:-4])
- month = int(date_str[-4:-2])
- day = int(date_str[-2:])
- # RFC 3977 doesn't say how to interpret 2-char years. Assume that
- # there are no dates before 1970 on Usenet.
- if year < 70:
- year += 2000
- elif year < 100:
- year += 1900
- return datetime.datetime(year, month, day, hours, minutes, seconds)
-
-def _unparse_datetime(dt, legacy=False):
- """Format a date or datetime object as a pair of (date, time) strings
- in the format required by the NEWNEWS and NEWGROUPS commands. If a
- date object is passed, the time is assumed to be midnight (00h00).
-
- The returned representation depends on the legacy flag:
- * if legacy is False (the default):
- date has the YYYYMMDD format and time the HHMMSS format
- * if legacy is True:
- date has the YYMMDD format and time the HHMMSS format.
- RFC 3977 compliant servers should understand both formats; therefore,
- legacy is only needed when talking to old servers.
- """
- if not isinstance(dt, datetime.datetime):
- time_str = "000000"
- else:
- time_str = "{0.hour:02d}{0.minute:02d}{0.second:02d}".format(dt)
- y = dt.year
- if legacy:
- y = y % 100
- date_str = "{0:02d}{1.month:02d}{1.day:02d}".format(y, dt)
- else:
- date_str = "{0:04d}{1.month:02d}{1.day:02d}".format(y, dt)
- return date_str, time_str
-
-
-if _have_ssl:
-
- def _encrypt_on(sock, context, hostname):
- """Wrap a socket in SSL/TLS. Arguments:
- - sock: Socket to wrap
- - context: SSL context to use for the encrypted connection
- Returns:
- - sock: New, encrypted socket.
- """
- # Generate a default SSL context if none was passed.
- if context is None:
- context = ssl._create_stdlib_context()
- return context.wrap_socket(sock, server_hostname=hostname)
-
-
-# The classes themselves
+
+try:
+ import ssl
+except ImportError:
+ _have_ssl = False
+else:
+ _have_ssl = True
+
+from email.header import decode_header as _email_decode_header
+from socket import _GLOBAL_DEFAULT_TIMEOUT
+
+__all__ = ["NNTP",
+ "NNTPError", "NNTPReplyError", "NNTPTemporaryError",
+ "NNTPPermanentError", "NNTPProtocolError", "NNTPDataError",
+ "decode_header",
+ ]
+
+# maximal line length when calling readline(). This is to prevent
+# reading arbitrary length lines. RFC 3977 limits NNTP line length to
+# 512 characters, including CRLF. We have selected 2048 just to be on
+# the safe side.
+_MAXLINE = 2048
+
+
+# Exceptions raised when an error or invalid response is received
+class NNTPError(Exception):
+ """Base class for all nntplib exceptions"""
+ def __init__(self, *args):
+ Exception.__init__(self, *args)
+ try:
+ self.response = args[0]
+ except IndexError:
+ self.response = 'No response given'
+
+class NNTPReplyError(NNTPError):
+ """Unexpected [123]xx reply"""
+ pass
+
+class NNTPTemporaryError(NNTPError):
+ """4xx errors"""
+ pass
+
+class NNTPPermanentError(NNTPError):
+ """5xx errors"""
+ pass
+
+class NNTPProtocolError(NNTPError):
+ """Response does not begin with [1-5]"""
+ pass
+
+class NNTPDataError(NNTPError):
+ """Error in response data"""
+ pass
+
+
+# Standard port used by NNTP servers
+NNTP_PORT = 119
+NNTP_SSL_PORT = 563
+
+# Response numbers that are followed by additional text (e.g. article)
+_LONGRESP = {
+ '100', # HELP
+ '101', # CAPABILITIES
+ '211', # LISTGROUP (also not multi-line with GROUP)
+ '215', # LIST
+ '220', # ARTICLE
+ '221', # HEAD, XHDR
+ '222', # BODY
+ '224', # OVER, XOVER
+ '225', # HDR
+ '230', # NEWNEWS
+ '231', # NEWGROUPS
+ '282', # XGTITLE
+}
+
+# Default decoded value for LIST OVERVIEW.FMT if not supported
+_DEFAULT_OVERVIEW_FMT = [
+ "subject", "from", "date", "message-id", "references", ":bytes", ":lines"]
+
+# Alternative names allowed in LIST OVERVIEW.FMT response
+_OVERVIEW_FMT_ALTERNATIVES = {
+ 'bytes': ':bytes',
+ 'lines': ':lines',
+}
+
+# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
+_CRLF = b'\r\n'
+
+GroupInfo = collections.namedtuple('GroupInfo',
+ ['group', 'last', 'first', 'flag'])
+
+ArticleInfo = collections.namedtuple('ArticleInfo',
+ ['number', 'message_id', 'lines'])
+
+
+# Helper function(s)
+def decode_header(header_str):
+ """Takes a unicode string representing a munged header value
+ and decodes it as a (possibly non-ASCII) readable value."""
+ parts = []
+ for v, enc in _email_decode_header(header_str):
+ if isinstance(v, bytes):
+ parts.append(v.decode(enc or 'ascii'))
+ else:
+ parts.append(v)
+ return ''.join(parts)
+
+def _parse_overview_fmt(lines):
+ """Parse a list of string representing the response to LIST OVERVIEW.FMT
+ and return a list of header/metadata names.
+ Raises NNTPDataError if the response is not compliant
+ (cf. RFC 3977, section 8.4)."""
+ fmt = []
+ for line in lines:
+ if line[0] == ':':
+ # Metadata name (e.g. ":bytes")
+ name, _, suffix = line[1:].partition(':')
+ name = ':' + name
+ else:
+ # Header name (e.g. "Subject:" or "Xref:full")
+ name, _, suffix = line.partition(':')
+ name = name.lower()
+ name = _OVERVIEW_FMT_ALTERNATIVES.get(name, name)
+ # Should we do something with the suffix?
+ fmt.append(name)
+ defaults = _DEFAULT_OVERVIEW_FMT
+ if len(fmt) < len(defaults):
+ raise NNTPDataError("LIST OVERVIEW.FMT response too short")
+ if fmt[:len(defaults)] != defaults:
+ raise NNTPDataError("LIST OVERVIEW.FMT redefines default fields")
+ return fmt
+
+def _parse_overview(lines, fmt, data_process_func=None):
+ """Parse the response to an OVER or XOVER command according to the
+ overview format `fmt`."""
+ n_defaults = len(_DEFAULT_OVERVIEW_FMT)
+ overview = []
+ for line in lines:
+ fields = {}
+ article_number, *tokens = line.split('\t')
+ article_number = int(article_number)
+ for i, token in enumerate(tokens):
+ if i >= len(fmt):
+ # XXX should we raise an error? Some servers might not
+ # support LIST OVERVIEW.FMT and still return additional
+ # headers.
+ continue
+ field_name = fmt[i]
+ is_metadata = field_name.startswith(':')
+ if i >= n_defaults and not is_metadata:
+ # Non-default header names are included in full in the response
+ # (unless the field is totally empty)
+ h = field_name + ": "
+ if token and token[:len(h)].lower() != h:
+ raise NNTPDataError("OVER/XOVER response doesn't include "
+ "names of additional headers")
+ token = token[len(h):] if token else None
+ fields[fmt[i]] = token
+ overview.append((article_number, fields))
+ return overview
+
+def _parse_datetime(date_str, time_str=None):
+ """Parse a pair of (date, time) strings, and return a datetime object.
+ If only the date is given, it is assumed to be date and time
+ concatenated together (e.g. response to the DATE command).
+ """
+ if time_str is None:
+ time_str = date_str[-6:]
+ date_str = date_str[:-6]
+ hours = int(time_str[:2])
+ minutes = int(time_str[2:4])
+ seconds = int(time_str[4:])
+ year = int(date_str[:-4])
+ month = int(date_str[-4:-2])
+ day = int(date_str[-2:])
+ # RFC 3977 doesn't say how to interpret 2-char years. Assume that
+ # there are no dates before 1970 on Usenet.
+ if year < 70:
+ year += 2000
+ elif year < 100:
+ year += 1900
+ return datetime.datetime(year, month, day, hours, minutes, seconds)
+
+def _unparse_datetime(dt, legacy=False):
+ """Format a date or datetime object as a pair of (date, time) strings
+ in the format required by the NEWNEWS and NEWGROUPS commands. If a
+ date object is passed, the time is assumed to be midnight (00h00).
+
+ The returned representation depends on the legacy flag:
+ * if legacy is False (the default):
+ date has the YYYYMMDD format and time the HHMMSS format
+ * if legacy is True:
+ date has the YYMMDD format and time the HHMMSS format.
+ RFC 3977 compliant servers should understand both formats; therefore,
+ legacy is only needed when talking to old servers.
+ """
+ if not isinstance(dt, datetime.datetime):
+ time_str = "000000"
+ else:
+ time_str = "{0.hour:02d}{0.minute:02d}{0.second:02d}".format(dt)
+ y = dt.year
+ if legacy:
+ y = y % 100
+ date_str = "{0:02d}{1.month:02d}{1.day:02d}".format(y, dt)
+ else:
+ date_str = "{0:04d}{1.month:02d}{1.day:02d}".format(y, dt)
+ return date_str, time_str
+
+
+if _have_ssl:
+
+ def _encrypt_on(sock, context, hostname):
+ """Wrap a socket in SSL/TLS. Arguments:
+ - sock: Socket to wrap
+ - context: SSL context to use for the encrypted connection
+ Returns:
+ - sock: New, encrypted socket.
+ """
+ # Generate a default SSL context if none was passed.
+ if context is None:
+ context = ssl._create_stdlib_context()
+ return context.wrap_socket(sock, server_hostname=hostname)
+
+
+# The classes themselves
class NNTP:
- # UTF-8 is the character set for all NNTP commands and responses: they
- # are automatically encoded (when sending) and decoded (and receiving)
- # by this class.
- # However, some multi-line data blocks can contain arbitrary bytes (for
- # example, latin-1 or utf-16 data in the body of a message). Commands
- # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
- # data will therefore only accept and produce bytes objects.
- # Furthermore, since there could be non-compliant servers out there,
- # we use 'surrogateescape' as the error handler for fault tolerance
- # and easy round-tripping. This could be useful for some applications
- # (e.g. NNTP gateways).
-
- encoding = 'utf-8'
- errors = 'surrogateescape'
-
+ # UTF-8 is the character set for all NNTP commands and responses: they
+ # are automatically encoded (when sending) and decoded (and receiving)
+ # by this class.
+ # However, some multi-line data blocks can contain arbitrary bytes (for
+ # example, latin-1 or utf-16 data in the body of a message). Commands
+ # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
+ # data will therefore only accept and produce bytes objects.
+ # Furthermore, since there could be non-compliant servers out there,
+ # we use 'surrogateescape' as the error handler for fault tolerance
+ # and easy round-tripping. This could be useful for some applications
+ # (e.g. NNTP gateways).
+
+ encoding = 'utf-8'
+ errors = 'surrogateescape'
+
def __init__(self, host, port=NNTP_PORT, user=None, password=None,
readermode=None, usenetrc=False,
timeout=_GLOBAL_DEFAULT_TIMEOUT):
- """Initialize an instance. Arguments:
+ """Initialize an instance. Arguments:
- host: hostname to connect to
- port: port to connect to (default the standard NNTP port)
- user: username to authenticate with
- password: password to use with username
- - readermode: if true, send 'mode reader' command after
- connecting.
+ - readermode: if true, send 'mode reader' command after
+ connecting.
- usenetrc: allow loading username and password from ~/.netrc file
if not specified explicitly
- - timeout: timeout (in seconds) used for socket connections
-
- readermode is sometimes necessary if you are connecting to an
- NNTP server on the local machine and intend to call
- reader-specific commands, such as `group'. If you get
- unexpected NNTPPermanentErrors, you might need to set
- readermode.
- """
- self.host = host
+ - timeout: timeout (in seconds) used for socket connections
+
+ readermode is sometimes necessary if you are connecting to an
+ NNTP server on the local machine and intend to call
+ reader-specific commands, such as `group'. If you get
+ unexpected NNTPPermanentErrors, you might need to set
+ readermode.
+ """
+ self.host = host
self.port = port
self.sock = self._create_socket(timeout)
self.file = None
@@ -348,743 +348,743 @@ class NNTP:
"""Partial initialization for the NNTP protocol.
This instance method is extracted for supporting the test code.
"""
- self.debugging = 0
- self.welcome = self._getresp()
-
- # Inquire about capabilities (RFC 3977).
- self._caps = None
- self.getcapabilities()
-
- # 'MODE READER' is sometimes necessary to enable 'reader' mode.
- # However, the order in which 'MODE READER' and 'AUTHINFO' need to
- # arrive differs between some NNTP servers. If _setreadermode() fails
- # with an authorization failed error, it will set this to True;
- # the login() routine will interpret that as a request to try again
- # after performing its normal function.
- # Enable only if we're not already in READER mode anyway.
- self.readermode_afterauth = False
- if readermode and 'READER' not in self._caps:
- self._setreadermode()
- if not self.readermode_afterauth:
- # Capabilities might have changed after MODE READER
- self._caps = None
- self.getcapabilities()
-
- # RFC 4642 2.2.2: Both the client and the server MUST know if there is
- # a TLS session active. A client MUST NOT attempt to start a TLS
- # session if a TLS session is already active.
- self.tls_on = False
-
- # Log in and encryption setup order is left to subclasses.
- self.authenticated = False
-
- def __enter__(self):
- return self
-
- def __exit__(self, *args):
- is_connected = lambda: hasattr(self, "file")
- if is_connected():
- try:
- self.quit()
- except (OSError, EOFError):
- pass
- finally:
- if is_connected():
- self._close()
-
+ self.debugging = 0
+ self.welcome = self._getresp()
+
+ # Inquire about capabilities (RFC 3977).
+ self._caps = None
+ self.getcapabilities()
+
+ # 'MODE READER' is sometimes necessary to enable 'reader' mode.
+ # However, the order in which 'MODE READER' and 'AUTHINFO' need to
+ # arrive differs between some NNTP servers. If _setreadermode() fails
+ # with an authorization failed error, it will set this to True;
+ # the login() routine will interpret that as a request to try again
+ # after performing its normal function.
+ # Enable only if we're not already in READER mode anyway.
+ self.readermode_afterauth = False
+ if readermode and 'READER' not in self._caps:
+ self._setreadermode()
+ if not self.readermode_afterauth:
+ # Capabilities might have changed after MODE READER
+ self._caps = None
+ self.getcapabilities()
+
+ # RFC 4642 2.2.2: Both the client and the server MUST know if there is
+ # a TLS session active. A client MUST NOT attempt to start a TLS
+ # session if a TLS session is already active.
+ self.tls_on = False
+
+ # Log in and encryption setup order is left to subclasses.
+ self.authenticated = False
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ is_connected = lambda: hasattr(self, "file")
+ if is_connected():
+ try:
+ self.quit()
+ except (OSError, EOFError):
+ pass
+ finally:
+ if is_connected():
+ self._close()
+
def _create_socket(self, timeout):
if timeout is not None and not timeout:
raise ValueError('Non-blocking socket (timeout=0) is not supported')
sys.audit("nntplib.connect", self, self.host, self.port)
return socket.create_connection((self.host, self.port), timeout)
- def getwelcome(self):
- """Get the welcome message from the server
- (this is read and squirreled away by __init__()).
- If the response code is 200, posting is allowed;
- if it 201, posting is not allowed."""
-
- if self.debugging: print('*welcome*', repr(self.welcome))
- return self.welcome
-
- def getcapabilities(self):
- """Get the server capabilities, as read by __init__().
- If the CAPABILITIES command is not supported, an empty dict is
- returned."""
- if self._caps is None:
- self.nntp_version = 1
- self.nntp_implementation = None
- try:
- resp, caps = self.capabilities()
- except (NNTPPermanentError, NNTPTemporaryError):
- # Server doesn't support capabilities
- self._caps = {}
- else:
- self._caps = caps
- if 'VERSION' in caps:
- # The server can advertise several supported versions,
- # choose the highest.
- self.nntp_version = max(map(int, caps['VERSION']))
- if 'IMPLEMENTATION' in caps:
- self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
- return self._caps
-
- def set_debuglevel(self, level):
- """Set the debugging level. Argument 'level' means:
- 0: no debugging output (default)
- 1: print commands and responses but not body text etc.
- 2: also print raw lines read and sent before stripping CR/LF"""
-
- self.debugging = level
- debug = set_debuglevel
-
- def _putline(self, line):
- """Internal: send one line to the server, appending CRLF.
- The `line` must be a bytes-like object."""
+ def getwelcome(self):
+ """Get the welcome message from the server
+ (this is read and squirreled away by __init__()).
+ If the response code is 200, posting is allowed;
+ if it 201, posting is not allowed."""
+
+ if self.debugging: print('*welcome*', repr(self.welcome))
+ return self.welcome
+
+ def getcapabilities(self):
+ """Get the server capabilities, as read by __init__().
+ If the CAPABILITIES command is not supported, an empty dict is
+ returned."""
+ if self._caps is None:
+ self.nntp_version = 1
+ self.nntp_implementation = None
+ try:
+ resp, caps = self.capabilities()
+ except (NNTPPermanentError, NNTPTemporaryError):
+ # Server doesn't support capabilities
+ self._caps = {}
+ else:
+ self._caps = caps
+ if 'VERSION' in caps:
+ # The server can advertise several supported versions,
+ # choose the highest.
+ self.nntp_version = max(map(int, caps['VERSION']))
+ if 'IMPLEMENTATION' in caps:
+ self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
+ return self._caps
+
+ def set_debuglevel(self, level):
+ """Set the debugging level. Argument 'level' means:
+ 0: no debugging output (default)
+ 1: print commands and responses but not body text etc.
+ 2: also print raw lines read and sent before stripping CR/LF"""
+
+ self.debugging = level
+ debug = set_debuglevel
+
+ def _putline(self, line):
+ """Internal: send one line to the server, appending CRLF.
+ The `line` must be a bytes-like object."""
sys.audit("nntplib.putline", self, line)
- line = line + _CRLF
- if self.debugging > 1: print('*put*', repr(line))
- self.file.write(line)
- self.file.flush()
-
- def _putcmd(self, line):
- """Internal: send one command to the server (through _putline()).
- The `line` must be a unicode string."""
- if self.debugging: print('*cmd*', repr(line))
- line = line.encode(self.encoding, self.errors)
- self._putline(line)
-
- def _getline(self, strip_crlf=True):
- """Internal: return one line from the server, stripping _CRLF.
- Raise EOFError if the connection is closed.
- Returns a bytes object."""
- line = self.file.readline(_MAXLINE +1)
- if len(line) > _MAXLINE:
- raise NNTPDataError('line too long')
- if self.debugging > 1:
- print('*get*', repr(line))
- if not line: raise EOFError
- if strip_crlf:
- if line[-2:] == _CRLF:
- line = line[:-2]
- elif line[-1:] in _CRLF:
- line = line[:-1]
- return line
-
- def _getresp(self):
- """Internal: get a response from the server.
- Raise various errors if the response indicates an error.
- Returns a unicode string."""
- resp = self._getline()
- if self.debugging: print('*resp*', repr(resp))
- resp = resp.decode(self.encoding, self.errors)
- c = resp[:1]
- if c == '4':
- raise NNTPTemporaryError(resp)
- if c == '5':
- raise NNTPPermanentError(resp)
- if c not in '123':
- raise NNTPProtocolError(resp)
- return resp
-
- def _getlongresp(self, file=None):
- """Internal: get a response plus following text from the server.
- Raise various errors if the response indicates an error.
-
- Returns a (response, lines) tuple where `response` is a unicode
- string and `lines` is a list of bytes objects.
- If `file` is a file-like object, it must be open in binary mode.
- """
-
- openedFile = None
- try:
- # If a string was passed then open a file with that name
- if isinstance(file, (str, bytes)):
- openedFile = file = open(file, "wb")
-
- resp = self._getresp()
- if resp[:3] not in _LONGRESP:
- raise NNTPReplyError(resp)
-
- lines = []
- if file is not None:
- # XXX lines = None instead?
- terminators = (b'.' + _CRLF, b'.\n')
- while 1:
- line = self._getline(False)
- if line in terminators:
- break
- if line.startswith(b'..'):
- line = line[1:]
- file.write(line)
- else:
- terminator = b'.'
- while 1:
- line = self._getline()
- if line == terminator:
- break
- if line.startswith(b'..'):
- line = line[1:]
- lines.append(line)
- finally:
- # If this method created the file, then it must close it
- if openedFile:
- openedFile.close()
-
- return resp, lines
-
- def _shortcmd(self, line):
- """Internal: send a command and get the response.
- Same return value as _getresp()."""
- self._putcmd(line)
- return self._getresp()
-
- def _longcmd(self, line, file=None):
- """Internal: send a command and get the response plus following text.
- Same return value as _getlongresp()."""
- self._putcmd(line)
- return self._getlongresp(file)
-
- def _longcmdstring(self, line, file=None):
- """Internal: send a command and get the response plus following text.
- Same as _longcmd() and _getlongresp(), except that the returned `lines`
- are unicode strings rather than bytes objects.
- """
- self._putcmd(line)
- resp, list = self._getlongresp(file)
- return resp, [line.decode(self.encoding, self.errors)
- for line in list]
-
- def _getoverviewfmt(self):
- """Internal: get the overview format. Queries the server if not
- already done, else returns the cached value."""
- try:
- return self._cachedoverviewfmt
- except AttributeError:
- pass
- try:
- resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
- except NNTPPermanentError:
- # Not supported by server?
- fmt = _DEFAULT_OVERVIEW_FMT[:]
- else:
- fmt = _parse_overview_fmt(lines)
- self._cachedoverviewfmt = fmt
- return fmt
-
- def _grouplist(self, lines):
- # Parse lines into "group last first flag"
- return [GroupInfo(*line.split()) for line in lines]
-
- def capabilities(self):
- """Process a CAPABILITIES command. Not supported by all servers.
- Return:
- - resp: server response if successful
- - caps: a dictionary mapping capability names to lists of tokens
- (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
- """
- caps = {}
- resp, lines = self._longcmdstring("CAPABILITIES")
- for line in lines:
- name, *tokens = line.split()
- caps[name] = tokens
- return resp, caps
-
- def newgroups(self, date, *, file=None):
- """Process a NEWGROUPS command. Arguments:
- - date: a date or datetime object
- Return:
- - resp: server response if successful
- - list: list of newsgroup names
- """
- if not isinstance(date, (datetime.date, datetime.date)):
- raise TypeError(
- "the date parameter must be a date or datetime object, "
- "not '{:40}'".format(date.__class__.__name__))
- date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
- cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
- resp, lines = self._longcmdstring(cmd, file)
- return resp, self._grouplist(lines)
-
- def newnews(self, group, date, *, file=None):
- """Process a NEWNEWS command. Arguments:
- - group: group name or '*'
- - date: a date or datetime object
- Return:
- - resp: server response if successful
- - list: list of message ids
- """
- if not isinstance(date, (datetime.date, datetime.date)):
- raise TypeError(
- "the date parameter must be a date or datetime object, "
- "not '{:40}'".format(date.__class__.__name__))
- date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
- cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
- return self._longcmdstring(cmd, file)
-
- def list(self, group_pattern=None, *, file=None):
- """Process a LIST or LIST ACTIVE command. Arguments:
- - group_pattern: a pattern indicating which groups to query
- - file: Filename string or file object to store the result in
- Returns:
- - resp: server response if successful
- - list: list of (group, last, first, flag) (strings)
- """
- if group_pattern is not None:
- command = 'LIST ACTIVE ' + group_pattern
- else:
- command = 'LIST'
- resp, lines = self._longcmdstring(command, file)
- return resp, self._grouplist(lines)
-
- def _getdescriptions(self, group_pattern, return_all):
- line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
- # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
- resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
- if not resp.startswith('215'):
- # Now the deprecated XGTITLE. This either raises an error
- # or succeeds with the same output structure as LIST
- # NEWSGROUPS.
- resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
- groups = {}
- for raw_line in lines:
- match = line_pat.search(raw_line.strip())
- if match:
- name, desc = match.group(1, 2)
- if not return_all:
- return desc
- groups[name] = desc
- if return_all:
- return resp, groups
- else:
- # Nothing found
- return ''
-
- def description(self, group):
- """Get a description for a single group. If more than one
- group matches ('group' is a pattern), return the first. If no
- group matches, return an empty string.
-
- This elides the response code from the server, since it can
- only be '215' or '285' (for xgtitle) anyway. If the response
- code is needed, use the 'descriptions' method.
-
- NOTE: This neither checks for a wildcard in 'group' nor does
- it check whether the group actually exists."""
- return self._getdescriptions(group, False)
-
- def descriptions(self, group_pattern):
- """Get descriptions for a range of groups."""
- return self._getdescriptions(group_pattern, True)
-
- def group(self, name):
- """Process a GROUP command. Argument:
- - group: the group name
- Returns:
- - resp: server response if successful
- - count: number of articles
- - first: first article number
- - last: last article number
- - name: the group name
- """
- resp = self._shortcmd('GROUP ' + name)
- if not resp.startswith('211'):
- raise NNTPReplyError(resp)
- words = resp.split()
- count = first = last = 0
- n = len(words)
- if n > 1:
- count = words[1]
- if n > 2:
- first = words[2]
- if n > 3:
- last = words[3]
- if n > 4:
- name = words[4].lower()
- return resp, int(count), int(first), int(last), name
-
- def help(self, *, file=None):
- """Process a HELP command. Argument:
- - file: Filename string or file object to store the result in
- Returns:
- - resp: server response if successful
- - list: list of strings returned by the server in response to the
- HELP command
- """
- return self._longcmdstring('HELP', file)
-
- def _statparse(self, resp):
- """Internal: parse the response line of a STAT, NEXT, LAST,
- ARTICLE, HEAD or BODY command."""
- if not resp.startswith('22'):
- raise NNTPReplyError(resp)
- words = resp.split()
- art_num = int(words[1])
- message_id = words[2]
- return resp, art_num, message_id
-
- def _statcmd(self, line):
- """Internal: process a STAT, NEXT or LAST command."""
- resp = self._shortcmd(line)
- return self._statparse(resp)
-
- def stat(self, message_spec=None):
- """Process a STAT command. Argument:
- - message_spec: article number or message id (if not specified,
- the current article is selected)
- Returns:
- - resp: server response if successful
- - art_num: the article number
- - message_id: the message id
- """
- if message_spec:
- return self._statcmd('STAT {0}'.format(message_spec))
- else:
- return self._statcmd('STAT')
-
- def next(self):
- """Process a NEXT command. No arguments. Return as for STAT."""
- return self._statcmd('NEXT')
-
- def last(self):
- """Process a LAST command. No arguments. Return as for STAT."""
- return self._statcmd('LAST')
-
- def _artcmd(self, line, file=None):
- """Internal: process a HEAD, BODY or ARTICLE command."""
- resp, lines = self._longcmd(line, file)
- resp, art_num, message_id = self._statparse(resp)
- return resp, ArticleInfo(art_num, message_id, lines)
-
- def head(self, message_spec=None, *, file=None):
- """Process a HEAD command. Argument:
- - message_spec: article number or message id
- - file: filename string or file object to store the headers in
- Returns:
- - resp: server response if successful
- - ArticleInfo: (article number, message id, list of header lines)
- """
- if message_spec is not None:
- cmd = 'HEAD {0}'.format(message_spec)
- else:
- cmd = 'HEAD'
- return self._artcmd(cmd, file)
-
- def body(self, message_spec=None, *, file=None):
- """Process a BODY command. Argument:
- - message_spec: article number or message id
- - file: filename string or file object to store the body in
- Returns:
- - resp: server response if successful
- - ArticleInfo: (article number, message id, list of body lines)
- """
- if message_spec is not None:
- cmd = 'BODY {0}'.format(message_spec)
- else:
- cmd = 'BODY'
- return self._artcmd(cmd, file)
-
- def article(self, message_spec=None, *, file=None):
- """Process an ARTICLE command. Argument:
- - message_spec: article number or message id
- - file: filename string or file object to store the article in
- Returns:
- - resp: server response if successful
- - ArticleInfo: (article number, message id, list of article lines)
- """
- if message_spec is not None:
- cmd = 'ARTICLE {0}'.format(message_spec)
- else:
- cmd = 'ARTICLE'
- return self._artcmd(cmd, file)
-
- def slave(self):
- """Process a SLAVE command. Returns:
- - resp: server response if successful
- """
- return self._shortcmd('SLAVE')
-
- def xhdr(self, hdr, str, *, file=None):
- """Process an XHDR command (optional server extension). Arguments:
- - hdr: the header type (e.g. 'subject')
- - str: an article nr, a message id, or a range nr1-nr2
- - file: Filename string or file object to store the result in
- Returns:
- - resp: server response if successful
- - list: list of (nr, value) strings
- """
- pat = re.compile('^([0-9]+) ?(.*)\n?')
- resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
- def remove_number(line):
- m = pat.match(line)
- return m.group(1, 2) if m else line
- return resp, [remove_number(line) for line in lines]
-
- def xover(self, start, end, *, file=None):
- """Process an XOVER command (optional server extension) Arguments:
- - start: start of range
- - end: end of range
- - file: Filename string or file object to store the result in
- Returns:
- - resp: server response if successful
- - list: list of dicts containing the response fields
- """
- resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
- file)
- fmt = self._getoverviewfmt()
- return resp, _parse_overview(lines, fmt)
-
- def over(self, message_spec, *, file=None):
- """Process an OVER command. If the command isn't supported, fall
- back to XOVER. Arguments:
- - message_spec:
- - either a message id, indicating the article to fetch
- information about
- - or a (start, end) tuple, indicating a range of article numbers;
- if end is None, information up to the newest message will be
- retrieved
- - or None, indicating the current article number must be used
- - file: Filename string or file object to store the result in
- Returns:
- - resp: server response if successful
- - list: list of dicts containing the response fields
-
- NOTE: the "message id" form isn't supported by XOVER
- """
- cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
- if isinstance(message_spec, (tuple, list)):
- start, end = message_spec
- cmd += ' {0}-{1}'.format(start, end or '')
- elif message_spec is not None:
- cmd = cmd + ' ' + message_spec
- resp, lines = self._longcmdstring(cmd, file)
- fmt = self._getoverviewfmt()
- return resp, _parse_overview(lines, fmt)
-
- def date(self):
- """Process the DATE command.
- Returns:
- - resp: server response if successful
- - date: datetime object
- """
- resp = self._shortcmd("DATE")
- if not resp.startswith('111'):
- raise NNTPReplyError(resp)
- elem = resp.split()
- if len(elem) != 2:
- raise NNTPDataError(resp)
- date = elem[1]
- if len(date) != 14:
- raise NNTPDataError(resp)
- return resp, _parse_datetime(date, None)
-
- def _post(self, command, f):
- resp = self._shortcmd(command)
- # Raises a specific exception if posting is not allowed
- if not resp.startswith('3'):
- raise NNTPReplyError(resp)
- if isinstance(f, (bytes, bytearray)):
- f = f.splitlines()
- # We don't use _putline() because:
- # - we don't want additional CRLF if the file or iterable is already
- # in the right format
- # - we don't want a spurious flush() after each line is written
- for line in f:
- if not line.endswith(_CRLF):
- line = line.rstrip(b"\r\n") + _CRLF
- if line.startswith(b'.'):
- line = b'.' + line
- self.file.write(line)
- self.file.write(b".\r\n")
- self.file.flush()
- return self._getresp()
-
- def post(self, data):
- """Process a POST command. Arguments:
- - data: bytes object, iterable or file containing the article
- Returns:
- - resp: server response if successful"""
- return self._post('POST', data)
-
- def ihave(self, message_id, data):
- """Process an IHAVE command. Arguments:
- - message_id: message-id of the article
- - data: file containing the article
- Returns:
- - resp: server response if successful
- Note that if the server refuses the article an exception is raised."""
- return self._post('IHAVE {0}'.format(message_id), data)
-
- def _close(self):
+ line = line + _CRLF
+ if self.debugging > 1: print('*put*', repr(line))
+ self.file.write(line)
+ self.file.flush()
+
+ def _putcmd(self, line):
+ """Internal: send one command to the server (through _putline()).
+ The `line` must be a unicode string."""
+ if self.debugging: print('*cmd*', repr(line))
+ line = line.encode(self.encoding, self.errors)
+ self._putline(line)
+
+ def _getline(self, strip_crlf=True):
+ """Internal: return one line from the server, stripping _CRLF.
+ Raise EOFError if the connection is closed.
+ Returns a bytes object."""
+ line = self.file.readline(_MAXLINE +1)
+ if len(line) > _MAXLINE:
+ raise NNTPDataError('line too long')
+ if self.debugging > 1:
+ print('*get*', repr(line))
+ if not line: raise EOFError
+ if strip_crlf:
+ if line[-2:] == _CRLF:
+ line = line[:-2]
+ elif line[-1:] in _CRLF:
+ line = line[:-1]
+ return line
+
+ def _getresp(self):
+ """Internal: get a response from the server.
+ Raise various errors if the response indicates an error.
+ Returns a unicode string."""
+ resp = self._getline()
+ if self.debugging: print('*resp*', repr(resp))
+ resp = resp.decode(self.encoding, self.errors)
+ c = resp[:1]
+ if c == '4':
+ raise NNTPTemporaryError(resp)
+ if c == '5':
+ raise NNTPPermanentError(resp)
+ if c not in '123':
+ raise NNTPProtocolError(resp)
+ return resp
+
+ def _getlongresp(self, file=None):
+ """Internal: get a response plus following text from the server.
+ Raise various errors if the response indicates an error.
+
+ Returns a (response, lines) tuple where `response` is a unicode
+ string and `lines` is a list of bytes objects.
+ If `file` is a file-like object, it must be open in binary mode.
+ """
+
+ openedFile = None
+ try:
+ # If a string was passed then open a file with that name
+ if isinstance(file, (str, bytes)):
+ openedFile = file = open(file, "wb")
+
+ resp = self._getresp()
+ if resp[:3] not in _LONGRESP:
+ raise NNTPReplyError(resp)
+
+ lines = []
+ if file is not None:
+ # XXX lines = None instead?
+ terminators = (b'.' + _CRLF, b'.\n')
+ while 1:
+ line = self._getline(False)
+ if line in terminators:
+ break
+ if line.startswith(b'..'):
+ line = line[1:]
+ file.write(line)
+ else:
+ terminator = b'.'
+ while 1:
+ line = self._getline()
+ if line == terminator:
+ break
+ if line.startswith(b'..'):
+ line = line[1:]
+ lines.append(line)
+ finally:
+ # If this method created the file, then it must close it
+ if openedFile:
+ openedFile.close()
+
+ return resp, lines
+
+ def _shortcmd(self, line):
+ """Internal: send a command and get the response.
+ Same return value as _getresp()."""
+ self._putcmd(line)
+ return self._getresp()
+
+ def _longcmd(self, line, file=None):
+ """Internal: send a command and get the response plus following text.
+ Same return value as _getlongresp()."""
+ self._putcmd(line)
+ return self._getlongresp(file)
+
+ def _longcmdstring(self, line, file=None):
+ """Internal: send a command and get the response plus following text.
+ Same as _longcmd() and _getlongresp(), except that the returned `lines`
+ are unicode strings rather than bytes objects.
+ """
+ self._putcmd(line)
+ resp, list = self._getlongresp(file)
+ return resp, [line.decode(self.encoding, self.errors)
+ for line in list]
+
+ def _getoverviewfmt(self):
+ """Internal: get the overview format. Queries the server if not
+ already done, else returns the cached value."""
+ try:
+ return self._cachedoverviewfmt
+ except AttributeError:
+ pass
+ try:
+ resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
+ except NNTPPermanentError:
+ # Not supported by server?
+ fmt = _DEFAULT_OVERVIEW_FMT[:]
+ else:
+ fmt = _parse_overview_fmt(lines)
+ self._cachedoverviewfmt = fmt
+ return fmt
+
+ def _grouplist(self, lines):
+ # Parse lines into "group last first flag"
+ return [GroupInfo(*line.split()) for line in lines]
+
+ def capabilities(self):
+ """Process a CAPABILITIES command. Not supported by all servers.
+ Return:
+ - resp: server response if successful
+ - caps: a dictionary mapping capability names to lists of tokens
+ (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
+ """
+ caps = {}
+ resp, lines = self._longcmdstring("CAPABILITIES")
+ for line in lines:
+ name, *tokens = line.split()
+ caps[name] = tokens
+ return resp, caps
+
+ def newgroups(self, date, *, file=None):
+ """Process a NEWGROUPS command. Arguments:
+ - date: a date or datetime object
+ Return:
+ - resp: server response if successful
+ - list: list of newsgroup names
+ """
+ if not isinstance(date, (datetime.date, datetime.date)):
+ raise TypeError(
+ "the date parameter must be a date or datetime object, "
+ "not '{:40}'".format(date.__class__.__name__))
+ date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
+ cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
+ resp, lines = self._longcmdstring(cmd, file)
+ return resp, self._grouplist(lines)
+
+ def newnews(self, group, date, *, file=None):
+ """Process a NEWNEWS command. Arguments:
+ - group: group name or '*'
+ - date: a date or datetime object
+ Return:
+ - resp: server response if successful
+ - list: list of message ids
+ """
+ if not isinstance(date, (datetime.date, datetime.date)):
+ raise TypeError(
+ "the date parameter must be a date or datetime object, "
+ "not '{:40}'".format(date.__class__.__name__))
+ date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
+ cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
+ return self._longcmdstring(cmd, file)
+
+ def list(self, group_pattern=None, *, file=None):
+ """Process a LIST or LIST ACTIVE command. Arguments:
+ - group_pattern: a pattern indicating which groups to query
+ - file: Filename string or file object to store the result in
+ Returns:
+ - resp: server response if successful
+ - list: list of (group, last, first, flag) (strings)
+ """
+ if group_pattern is not None:
+ command = 'LIST ACTIVE ' + group_pattern
+ else:
+ command = 'LIST'
+ resp, lines = self._longcmdstring(command, file)
+ return resp, self._grouplist(lines)
+
+ def _getdescriptions(self, group_pattern, return_all):
+ line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
+ # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
+ resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
+ if not resp.startswith('215'):
+ # Now the deprecated XGTITLE. This either raises an error
+ # or succeeds with the same output structure as LIST
+ # NEWSGROUPS.
+ resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
+ groups = {}
+ for raw_line in lines:
+ match = line_pat.search(raw_line.strip())
+ if match:
+ name, desc = match.group(1, 2)
+ if not return_all:
+ return desc
+ groups[name] = desc
+ if return_all:
+ return resp, groups
+ else:
+ # Nothing found
+ return ''
+
+ def description(self, group):
+ """Get a description for a single group. If more than one
+ group matches ('group' is a pattern), return the first. If no
+ group matches, return an empty string.
+
+ This elides the response code from the server, since it can
+ only be '215' or '285' (for xgtitle) anyway. If the response
+ code is needed, use the 'descriptions' method.
+
+ NOTE: This neither checks for a wildcard in 'group' nor does
+ it check whether the group actually exists."""
+ return self._getdescriptions(group, False)
+
+ def descriptions(self, group_pattern):
+ """Get descriptions for a range of groups."""
+ return self._getdescriptions(group_pattern, True)
+
+ def group(self, name):
+ """Process a GROUP command. Argument:
+ - group: the group name
+ Returns:
+ - resp: server response if successful
+ - count: number of articles
+ - first: first article number
+ - last: last article number
+ - name: the group name
+ """
+ resp = self._shortcmd('GROUP ' + name)
+ if not resp.startswith('211'):
+ raise NNTPReplyError(resp)
+ words = resp.split()
+ count = first = last = 0
+ n = len(words)
+ if n > 1:
+ count = words[1]
+ if n > 2:
+ first = words[2]
+ if n > 3:
+ last = words[3]
+ if n > 4:
+ name = words[4].lower()
+ return resp, int(count), int(first), int(last), name
+
+ def help(self, *, file=None):
+ """Process a HELP command. Argument:
+ - file: Filename string or file object to store the result in
+ Returns:
+ - resp: server response if successful
+ - list: list of strings returned by the server in response to the
+ HELP command
+ """
+ return self._longcmdstring('HELP', file)
+
+ def _statparse(self, resp):
+ """Internal: parse the response line of a STAT, NEXT, LAST,
+ ARTICLE, HEAD or BODY command."""
+ if not resp.startswith('22'):
+ raise NNTPReplyError(resp)
+ words = resp.split()
+ art_num = int(words[1])
+ message_id = words[2]
+ return resp, art_num, message_id
+
+ def _statcmd(self, line):
+ """Internal: process a STAT, NEXT or LAST command."""
+ resp = self._shortcmd(line)
+ return self._statparse(resp)
+
+ def stat(self, message_spec=None):
+ """Process a STAT command. Argument:
+ - message_spec: article number or message id (if not specified,
+ the current article is selected)
+ Returns:
+ - resp: server response if successful
+ - art_num: the article number
+ - message_id: the message id
+ """
+ if message_spec:
+ return self._statcmd('STAT {0}'.format(message_spec))
+ else:
+ return self._statcmd('STAT')
+
+ def next(self):
+ """Process a NEXT command. No arguments. Return as for STAT."""
+ return self._statcmd('NEXT')
+
+ def last(self):
+ """Process a LAST command. No arguments. Return as for STAT."""
+ return self._statcmd('LAST')
+
+ def _artcmd(self, line, file=None):
+ """Internal: process a HEAD, BODY or ARTICLE command."""
+ resp, lines = self._longcmd(line, file)
+ resp, art_num, message_id = self._statparse(resp)
+ return resp, ArticleInfo(art_num, message_id, lines)
+
+ def head(self, message_spec=None, *, file=None):
+ """Process a HEAD command. Argument:
+ - message_spec: article number or message id
+ - file: filename string or file object to store the headers in
+ Returns:
+ - resp: server response if successful
+ - ArticleInfo: (article number, message id, list of header lines)
+ """
+ if message_spec is not None:
+ cmd = 'HEAD {0}'.format(message_spec)
+ else:
+ cmd = 'HEAD'
+ return self._artcmd(cmd, file)
+
+ def body(self, message_spec=None, *, file=None):
+ """Process a BODY command. Argument:
+ - message_spec: article number or message id
+ - file: filename string or file object to store the body in
+ Returns:
+ - resp: server response if successful
+ - ArticleInfo: (article number, message id, list of body lines)
+ """
+ if message_spec is not None:
+ cmd = 'BODY {0}'.format(message_spec)
+ else:
+ cmd = 'BODY'
+ return self._artcmd(cmd, file)
+
+ def article(self, message_spec=None, *, file=None):
+ """Process an ARTICLE command. Argument:
+ - message_spec: article number or message id
+ - file: filename string or file object to store the article in
+ Returns:
+ - resp: server response if successful
+ - ArticleInfo: (article number, message id, list of article lines)
+ """
+ if message_spec is not None:
+ cmd = 'ARTICLE {0}'.format(message_spec)
+ else:
+ cmd = 'ARTICLE'
+ return self._artcmd(cmd, file)
+
+ def slave(self):
+ """Process a SLAVE command. Returns:
+ - resp: server response if successful
+ """
+ return self._shortcmd('SLAVE')
+
+ def xhdr(self, hdr, str, *, file=None):
+ """Process an XHDR command (optional server extension). Arguments:
+ - hdr: the header type (e.g. 'subject')
+ - str: an article nr, a message id, or a range nr1-nr2
+ - file: Filename string or file object to store the result in
+ Returns:
+ - resp: server response if successful
+ - list: list of (nr, value) strings
+ """
+ pat = re.compile('^([0-9]+) ?(.*)\n?')
+ resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
+ def remove_number(line):
+ m = pat.match(line)
+ return m.group(1, 2) if m else line
+ return resp, [remove_number(line) for line in lines]
+
+ def xover(self, start, end, *, file=None):
+ """Process an XOVER command (optional server extension) Arguments:
+ - start: start of range
+ - end: end of range
+ - file: Filename string or file object to store the result in
+ Returns:
+ - resp: server response if successful
+ - list: list of dicts containing the response fields
+ """
+ resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
+ file)
+ fmt = self._getoverviewfmt()
+ return resp, _parse_overview(lines, fmt)
+
+ def over(self, message_spec, *, file=None):
+ """Process an OVER command. If the command isn't supported, fall
+ back to XOVER. Arguments:
+ - message_spec:
+ - either a message id, indicating the article to fetch
+ information about
+ - or a (start, end) tuple, indicating a range of article numbers;
+ if end is None, information up to the newest message will be
+ retrieved
+ - or None, indicating the current article number must be used
+ - file: Filename string or file object to store the result in
+ Returns:
+ - resp: server response if successful
+ - list: list of dicts containing the response fields
+
+ NOTE: the "message id" form isn't supported by XOVER
+ """
+ cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
+ if isinstance(message_spec, (tuple, list)):
+ start, end = message_spec
+ cmd += ' {0}-{1}'.format(start, end or '')
+ elif message_spec is not None:
+ cmd = cmd + ' ' + message_spec
+ resp, lines = self._longcmdstring(cmd, file)
+ fmt = self._getoverviewfmt()
+ return resp, _parse_overview(lines, fmt)
+
+ def date(self):
+ """Process the DATE command.
+ Returns:
+ - resp: server response if successful
+ - date: datetime object
+ """
+ resp = self._shortcmd("DATE")
+ if not resp.startswith('111'):
+ raise NNTPReplyError(resp)
+ elem = resp.split()
+ if len(elem) != 2:
+ raise NNTPDataError(resp)
+ date = elem[1]
+ if len(date) != 14:
+ raise NNTPDataError(resp)
+ return resp, _parse_datetime(date, None)
+
+ def _post(self, command, f):
+ resp = self._shortcmd(command)
+ # Raises a specific exception if posting is not allowed
+ if not resp.startswith('3'):
+ raise NNTPReplyError(resp)
+ if isinstance(f, (bytes, bytearray)):
+ f = f.splitlines()
+ # We don't use _putline() because:
+ # - we don't want additional CRLF if the file or iterable is already
+ # in the right format
+ # - we don't want a spurious flush() after each line is written
+ for line in f:
+ if not line.endswith(_CRLF):
+ line = line.rstrip(b"\r\n") + _CRLF
+ if line.startswith(b'.'):
+ line = b'.' + line
+ self.file.write(line)
+ self.file.write(b".\r\n")
+ self.file.flush()
+ return self._getresp()
+
+ def post(self, data):
+ """Process a POST command. Arguments:
+ - data: bytes object, iterable or file containing the article
+ Returns:
+ - resp: server response if successful"""
+ return self._post('POST', data)
+
+ def ihave(self, message_id, data):
+ """Process an IHAVE command. Arguments:
+ - message_id: message-id of the article
+ - data: file containing the article
+ Returns:
+ - resp: server response if successful
+ Note that if the server refuses the article an exception is raised."""
+ return self._post('IHAVE {0}'.format(message_id), data)
+
+ def _close(self):
try:
if self.file:
self.file.close()
del self.file
finally:
self.sock.close()
-
- def quit(self):
- """Process a QUIT command and close the socket. Returns:
- - resp: server response if successful"""
- try:
- resp = self._shortcmd('QUIT')
- finally:
- self._close()
- return resp
-
- def login(self, user=None, password=None, usenetrc=True):
- if self.authenticated:
- raise ValueError("Already logged in.")
- if not user and not usenetrc:
- raise ValueError(
- "At least one of `user` and `usenetrc` must be specified")
- # If no login/password was specified but netrc was requested,
- # try to get them from ~/.netrc
- # Presume that if .netrc has an entry, NNRP authentication is required.
- try:
- if usenetrc and not user:
- import netrc
- credentials = netrc.netrc()
- auth = credentials.authenticators(self.host)
- if auth:
- user = auth[0]
- password = auth[2]
- except OSError:
- pass
- # Perform NNTP authentication if needed.
- if not user:
- return
- resp = self._shortcmd('authinfo user ' + user)
- if resp.startswith('381'):
- if not password:
- raise NNTPReplyError(resp)
- else:
- resp = self._shortcmd('authinfo pass ' + password)
- if not resp.startswith('281'):
- raise NNTPPermanentError(resp)
- # Capabilities might have changed after login
- self._caps = None
- self.getcapabilities()
- # Attempt to send mode reader if it was requested after login.
- # Only do so if we're not in reader mode already.
- if self.readermode_afterauth and 'READER' not in self._caps:
- self._setreadermode()
- # Capabilities might have changed after MODE READER
- self._caps = None
- self.getcapabilities()
-
- def _setreadermode(self):
- try:
- self.welcome = self._shortcmd('mode reader')
- except NNTPPermanentError:
- # Error 5xx, probably 'not implemented'
- pass
- except NNTPTemporaryError as e:
- if e.response.startswith('480'):
- # Need authorization before 'mode reader'
- self.readermode_afterauth = True
- else:
- raise
-
- if _have_ssl:
- def starttls(self, context=None):
- """Process a STARTTLS command. Arguments:
- - context: SSL context to use for the encrypted connection
- """
- # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if
- # a TLS session already exists.
- if self.tls_on:
- raise ValueError("TLS is already enabled.")
- if self.authenticated:
- raise ValueError("TLS cannot be started after authentication.")
- resp = self._shortcmd('STARTTLS')
- if resp.startswith('382'):
- self.file.close()
- self.sock = _encrypt_on(self.sock, context, self.host)
- self.file = self.sock.makefile("rwb")
- self.tls_on = True
- # Capabilities may change after TLS starts up, so ask for them
- # again.
- self._caps = None
- self.getcapabilities()
- else:
- raise NNTPError("TLS failed to start.")
-
-
-if _have_ssl:
+
+ def quit(self):
+ """Process a QUIT command and close the socket. Returns:
+ - resp: server response if successful"""
+ try:
+ resp = self._shortcmd('QUIT')
+ finally:
+ self._close()
+ return resp
+
+ def login(self, user=None, password=None, usenetrc=True):
+ if self.authenticated:
+ raise ValueError("Already logged in.")
+ if not user and not usenetrc:
+ raise ValueError(
+ "At least one of `user` and `usenetrc` must be specified")
+ # If no login/password was specified but netrc was requested,
+ # try to get them from ~/.netrc
+ # Presume that if .netrc has an entry, NNRP authentication is required.
+ try:
+ if usenetrc and not user:
+ import netrc
+ credentials = netrc.netrc()
+ auth = credentials.authenticators(self.host)
+ if auth:
+ user = auth[0]
+ password = auth[2]
+ except OSError:
+ pass
+ # Perform NNTP authentication if needed.
+ if not user:
+ return
+ resp = self._shortcmd('authinfo user ' + user)
+ if resp.startswith('381'):
+ if not password:
+ raise NNTPReplyError(resp)
+ else:
+ resp = self._shortcmd('authinfo pass ' + password)
+ if not resp.startswith('281'):
+ raise NNTPPermanentError(resp)
+ # Capabilities might have changed after login
+ self._caps = None
+ self.getcapabilities()
+ # Attempt to send mode reader if it was requested after login.
+ # Only do so if we're not in reader mode already.
+ if self.readermode_afterauth and 'READER' not in self._caps:
+ self._setreadermode()
+ # Capabilities might have changed after MODE READER
+ self._caps = None
+ self.getcapabilities()
+
+ def _setreadermode(self):
+ try:
+ self.welcome = self._shortcmd('mode reader')
+ except NNTPPermanentError:
+ # Error 5xx, probably 'not implemented'
+ pass
+ except NNTPTemporaryError as e:
+ if e.response.startswith('480'):
+ # Need authorization before 'mode reader'
+ self.readermode_afterauth = True
+ else:
+ raise
+
+ if _have_ssl:
+ def starttls(self, context=None):
+ """Process a STARTTLS command. Arguments:
+ - context: SSL context to use for the encrypted connection
+ """
+ # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if
+ # a TLS session already exists.
+ if self.tls_on:
+ raise ValueError("TLS is already enabled.")
+ if self.authenticated:
+ raise ValueError("TLS cannot be started after authentication.")
+ resp = self._shortcmd('STARTTLS')
+ if resp.startswith('382'):
+ self.file.close()
+ self.sock = _encrypt_on(self.sock, context, self.host)
+ self.file = self.sock.makefile("rwb")
+ self.tls_on = True
+ # Capabilities may change after TLS starts up, so ask for them
+ # again.
+ self._caps = None
+ self.getcapabilities()
+ else:
+ raise NNTPError("TLS failed to start.")
+
+
+if _have_ssl:
class NNTP_SSL(NNTP):
-
- def __init__(self, host, port=NNTP_SSL_PORT,
- user=None, password=None, ssl_context=None,
- readermode=None, usenetrc=False,
- timeout=_GLOBAL_DEFAULT_TIMEOUT):
- """This works identically to NNTP.__init__, except for the change
- in default port and the `ssl_context` argument for SSL connections.
- """
+
+ def __init__(self, host, port=NNTP_SSL_PORT,
+ user=None, password=None, ssl_context=None,
+ readermode=None, usenetrc=False,
+ timeout=_GLOBAL_DEFAULT_TIMEOUT):
+ """This works identically to NNTP.__init__, except for the change
+ in default port and the `ssl_context` argument for SSL connections.
+ """
self.ssl_context = ssl_context
super().__init__(host, port, user, password, readermode,
usenetrc, timeout)
def _create_socket(self, timeout):
sock = super()._create_socket(timeout)
- try:
+ try:
sock = _encrypt_on(sock, self.ssl_context, self.host)
- except:
+ except:
sock.close()
- raise
+ raise
else:
return sock
-
- __all__.append("NNTP_SSL")
-
-
-# Test retrieval when run as a script.
-if __name__ == '__main__':
- import argparse
-
- parser = argparse.ArgumentParser(description="""\
- nntplib built-in demo - display the latest articles in a newsgroup""")
- parser.add_argument('-g', '--group', default='gmane.comp.python.general',
- help='group to fetch messages from (default: %(default)s)')
+
+ __all__.append("NNTP_SSL")
+
+
+# Test retrieval when run as a script.
+if __name__ == '__main__':
+ import argparse
+
+ parser = argparse.ArgumentParser(description="""\
+ nntplib built-in demo - display the latest articles in a newsgroup""")
+ parser.add_argument('-g', '--group', default='gmane.comp.python.general',
+ help='group to fetch messages from (default: %(default)s)')
parser.add_argument('-s', '--server', default='news.gmane.io',
- help='NNTP server hostname (default: %(default)s)')
- parser.add_argument('-p', '--port', default=-1, type=int,
- help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT))
- parser.add_argument('-n', '--nb-articles', default=10, type=int,
- help='number of articles to fetch (default: %(default)s)')
- parser.add_argument('-S', '--ssl', action='store_true', default=False,
- help='use NNTP over SSL')
- args = parser.parse_args()
-
- port = args.port
- if not args.ssl:
- if port == -1:
- port = NNTP_PORT
- s = NNTP(host=args.server, port=port)
- else:
- if port == -1:
- port = NNTP_SSL_PORT
- s = NNTP_SSL(host=args.server, port=port)
-
- caps = s.getcapabilities()
- if 'STARTTLS' in caps:
- s.starttls()
- resp, count, first, last, name = s.group(args.group)
- print('Group', name, 'has', count, 'articles, range', first, 'to', last)
-
- def cut(s, lim):
- if len(s) > lim:
- s = s[:lim - 4] + "..."
- return s
-
- first = str(int(last) - args.nb_articles + 1)
- resp, overviews = s.xover(first, last)
- for artnum, over in overviews:
- author = decode_header(over['from']).split('<', 1)[0]
- subject = decode_header(over['subject'])
- lines = int(over[':lines'])
- print("{:7} {:20} {:42} ({})".format(
- artnum, cut(author, 20), cut(subject, 42), lines)
- )
-
- s.quit()
+ help='NNTP server hostname (default: %(default)s)')
+ parser.add_argument('-p', '--port', default=-1, type=int,
+ help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT))
+ parser.add_argument('-n', '--nb-articles', default=10, type=int,
+ help='number of articles to fetch (default: %(default)s)')
+ parser.add_argument('-S', '--ssl', action='store_true', default=False,
+ help='use NNTP over SSL')
+ args = parser.parse_args()
+
+ port = args.port
+ if not args.ssl:
+ if port == -1:
+ port = NNTP_PORT
+ s = NNTP(host=args.server, port=port)
+ else:
+ if port == -1:
+ port = NNTP_SSL_PORT
+ s = NNTP_SSL(host=args.server, port=port)
+
+ caps = s.getcapabilities()
+ if 'STARTTLS' in caps:
+ s.starttls()
+ resp, count, first, last, name = s.group(args.group)
+ print('Group', name, 'has', count, 'articles, range', first, 'to', last)
+
+ def cut(s, lim):
+ if len(s) > lim:
+ s = s[:lim - 4] + "..."
+ return s
+
+ first = str(int(last) - args.nb_articles + 1)
+ resp, overviews = s.xover(first, last)
+ for artnum, over in overviews:
+ author = decode_header(over['from']).split('<', 1)[0]
+ subject = decode_header(over['subject'])
+ lines = int(over[':lines'])
+ print("{:7} {:20} {:42} ({})".format(
+ artnum, cut(author, 20), cut(subject, 42), lines)
+ )
+
+ s.quit()