17db96d56Sopenharmony_ci"""An NNTP client class based on:
27db96d56Sopenharmony_ci- RFC 977: Network News Transfer Protocol
37db96d56Sopenharmony_ci- RFC 2980: Common NNTP Extensions
47db96d56Sopenharmony_ci- RFC 3977: Network News Transfer Protocol (version 2)
57db96d56Sopenharmony_ci
67db96d56Sopenharmony_ciExample:
77db96d56Sopenharmony_ci
87db96d56Sopenharmony_ci>>> from nntplib import NNTP
97db96d56Sopenharmony_ci>>> s = NNTP('news')
107db96d56Sopenharmony_ci>>> resp, count, first, last, name = s.group('comp.lang.python')
117db96d56Sopenharmony_ci>>> print('Group', name, 'has', count, 'articles, range', first, 'to', last)
127db96d56Sopenharmony_ciGroup comp.lang.python has 51 articles, range 5770 to 5821
137db96d56Sopenharmony_ci>>> resp, subs = s.xhdr('subject', '{0}-{1}'.format(first, last))
147db96d56Sopenharmony_ci>>> resp = s.quit()
157db96d56Sopenharmony_ci>>>
167db96d56Sopenharmony_ci
177db96d56Sopenharmony_ciHere 'resp' is the server response line.
187db96d56Sopenharmony_ciError responses are turned into exceptions.
197db96d56Sopenharmony_ci
207db96d56Sopenharmony_ciTo post an article from a file:
217db96d56Sopenharmony_ci>>> f = open(filename, 'rb') # file containing article, including header
227db96d56Sopenharmony_ci>>> resp = s.post(f)
237db96d56Sopenharmony_ci>>>
247db96d56Sopenharmony_ci
257db96d56Sopenharmony_ciFor descriptions of all methods, read the comments in the code below.
267db96d56Sopenharmony_ciNote that all arguments and return values representing article numbers
277db96d56Sopenharmony_ciare strings, not numbers, since they are rarely used for calculations.
287db96d56Sopenharmony_ci"""
297db96d56Sopenharmony_ci
307db96d56Sopenharmony_ci# RFC 977 by Brian Kantor and Phil Lapsley.
317db96d56Sopenharmony_ci# xover, xgtitle, xpath, date methods by Kevan Heydon
327db96d56Sopenharmony_ci
337db96d56Sopenharmony_ci# Incompatible changes from the 2.x nntplib:
347db96d56Sopenharmony_ci# - all commands are encoded as UTF-8 data (using the "surrogateescape"
357db96d56Sopenharmony_ci#   error handler), except for raw message data (POST, IHAVE)
367db96d56Sopenharmony_ci# - all responses are decoded as UTF-8 data (using the "surrogateescape"
377db96d56Sopenharmony_ci#   error handler), except for raw message data (ARTICLE, HEAD, BODY)
387db96d56Sopenharmony_ci# - the `file` argument to various methods is keyword-only
397db96d56Sopenharmony_ci#
407db96d56Sopenharmony_ci# - NNTP.date() returns a datetime object
417db96d56Sopenharmony_ci# - NNTP.newgroups() and NNTP.newnews() take a datetime (or date) object,
427db96d56Sopenharmony_ci#   rather than a pair of (date, time) strings.
437db96d56Sopenharmony_ci# - NNTP.newgroups() and NNTP.list() return a list of GroupInfo named tuples
447db96d56Sopenharmony_ci# - NNTP.descriptions() returns a dict mapping group names to descriptions
457db96d56Sopenharmony_ci# - NNTP.xover() returns a list of dicts mapping field names (header or metadata)
467db96d56Sopenharmony_ci#   to field values; each dict representing a message overview.
477db96d56Sopenharmony_ci# - NNTP.article(), NNTP.head() and NNTP.body() return a (response, ArticleInfo)
487db96d56Sopenharmony_ci#   tuple.
497db96d56Sopenharmony_ci# - the "internal" methods have been marked private (they now start with
507db96d56Sopenharmony_ci#   an underscore)
517db96d56Sopenharmony_ci
527db96d56Sopenharmony_ci# Other changes from the 2.x/3.1 nntplib:
537db96d56Sopenharmony_ci# - automatic querying of capabilities at connect
547db96d56Sopenharmony_ci# - New method NNTP.getcapabilities()
557db96d56Sopenharmony_ci# - New method NNTP.over()
567db96d56Sopenharmony_ci# - New helper function decode_header()
577db96d56Sopenharmony_ci# - NNTP.post() and NNTP.ihave() accept file objects, bytes-like objects and
587db96d56Sopenharmony_ci#   arbitrary iterables yielding lines.
597db96d56Sopenharmony_ci# - An extensive test suite :-)
607db96d56Sopenharmony_ci
617db96d56Sopenharmony_ci# TODO:
627db96d56Sopenharmony_ci# - return structured data (GroupInfo etc.) everywhere
637db96d56Sopenharmony_ci# - support HDR
647db96d56Sopenharmony_ci
657db96d56Sopenharmony_ci# Imports
667db96d56Sopenharmony_ciimport re
677db96d56Sopenharmony_ciimport socket
687db96d56Sopenharmony_ciimport collections
697db96d56Sopenharmony_ciimport datetime
707db96d56Sopenharmony_ciimport sys
717db96d56Sopenharmony_ciimport warnings
727db96d56Sopenharmony_ci
737db96d56Sopenharmony_citry:
747db96d56Sopenharmony_ci    import ssl
757db96d56Sopenharmony_ciexcept ImportError:
767db96d56Sopenharmony_ci    _have_ssl = False
777db96d56Sopenharmony_cielse:
787db96d56Sopenharmony_ci    _have_ssl = True
797db96d56Sopenharmony_ci
807db96d56Sopenharmony_cifrom email.header import decode_header as _email_decode_header
817db96d56Sopenharmony_cifrom socket import _GLOBAL_DEFAULT_TIMEOUT
827db96d56Sopenharmony_ci
837db96d56Sopenharmony_ci__all__ = ["NNTP",
847db96d56Sopenharmony_ci           "NNTPError", "NNTPReplyError", "NNTPTemporaryError",
857db96d56Sopenharmony_ci           "NNTPPermanentError", "NNTPProtocolError", "NNTPDataError",
867db96d56Sopenharmony_ci           "decode_header",
877db96d56Sopenharmony_ci           ]
887db96d56Sopenharmony_ci
897db96d56Sopenharmony_ciwarnings._deprecated(__name__, remove=(3, 13))
907db96d56Sopenharmony_ci
917db96d56Sopenharmony_ci# maximal line length when calling readline(). This is to prevent
927db96d56Sopenharmony_ci# reading arbitrary length lines. RFC 3977 limits NNTP line length to
937db96d56Sopenharmony_ci# 512 characters, including CRLF. We have selected 2048 just to be on
947db96d56Sopenharmony_ci# the safe side.
957db96d56Sopenharmony_ci_MAXLINE = 2048
967db96d56Sopenharmony_ci
977db96d56Sopenharmony_ci
987db96d56Sopenharmony_ci# Exceptions raised when an error or invalid response is received
997db96d56Sopenharmony_ciclass NNTPError(Exception):
1007db96d56Sopenharmony_ci    """Base class for all nntplib exceptions"""
1017db96d56Sopenharmony_ci    def __init__(self, *args):
1027db96d56Sopenharmony_ci        Exception.__init__(self, *args)
1037db96d56Sopenharmony_ci        try:
1047db96d56Sopenharmony_ci            self.response = args[0]
1057db96d56Sopenharmony_ci        except IndexError:
1067db96d56Sopenharmony_ci            self.response = 'No response given'
1077db96d56Sopenharmony_ci
1087db96d56Sopenharmony_ciclass NNTPReplyError(NNTPError):
1097db96d56Sopenharmony_ci    """Unexpected [123]xx reply"""
1107db96d56Sopenharmony_ci    pass
1117db96d56Sopenharmony_ci
1127db96d56Sopenharmony_ciclass NNTPTemporaryError(NNTPError):
1137db96d56Sopenharmony_ci    """4xx errors"""
1147db96d56Sopenharmony_ci    pass
1157db96d56Sopenharmony_ci
1167db96d56Sopenharmony_ciclass NNTPPermanentError(NNTPError):
1177db96d56Sopenharmony_ci    """5xx errors"""
1187db96d56Sopenharmony_ci    pass
1197db96d56Sopenharmony_ci
1207db96d56Sopenharmony_ciclass NNTPProtocolError(NNTPError):
1217db96d56Sopenharmony_ci    """Response does not begin with [1-5]"""
1227db96d56Sopenharmony_ci    pass
1237db96d56Sopenharmony_ci
1247db96d56Sopenharmony_ciclass NNTPDataError(NNTPError):
1257db96d56Sopenharmony_ci    """Error in response data"""
1267db96d56Sopenharmony_ci    pass
1277db96d56Sopenharmony_ci
1287db96d56Sopenharmony_ci
1297db96d56Sopenharmony_ci# Standard port used by NNTP servers
1307db96d56Sopenharmony_ciNNTP_PORT = 119
1317db96d56Sopenharmony_ciNNTP_SSL_PORT = 563
1327db96d56Sopenharmony_ci
1337db96d56Sopenharmony_ci# Response numbers that are followed by additional text (e.g. article)
1347db96d56Sopenharmony_ci_LONGRESP = {
1357db96d56Sopenharmony_ci    '100',   # HELP
1367db96d56Sopenharmony_ci    '101',   # CAPABILITIES
1377db96d56Sopenharmony_ci    '211',   # LISTGROUP   (also not multi-line with GROUP)
1387db96d56Sopenharmony_ci    '215',   # LIST
1397db96d56Sopenharmony_ci    '220',   # ARTICLE
1407db96d56Sopenharmony_ci    '221',   # HEAD, XHDR
1417db96d56Sopenharmony_ci    '222',   # BODY
1427db96d56Sopenharmony_ci    '224',   # OVER, XOVER
1437db96d56Sopenharmony_ci    '225',   # HDR
1447db96d56Sopenharmony_ci    '230',   # NEWNEWS
1457db96d56Sopenharmony_ci    '231',   # NEWGROUPS
1467db96d56Sopenharmony_ci    '282',   # XGTITLE
1477db96d56Sopenharmony_ci}
1487db96d56Sopenharmony_ci
1497db96d56Sopenharmony_ci# Default decoded value for LIST OVERVIEW.FMT if not supported
1507db96d56Sopenharmony_ci_DEFAULT_OVERVIEW_FMT = [
1517db96d56Sopenharmony_ci    "subject", "from", "date", "message-id", "references", ":bytes", ":lines"]
1527db96d56Sopenharmony_ci
1537db96d56Sopenharmony_ci# Alternative names allowed in LIST OVERVIEW.FMT response
1547db96d56Sopenharmony_ci_OVERVIEW_FMT_ALTERNATIVES = {
1557db96d56Sopenharmony_ci    'bytes': ':bytes',
1567db96d56Sopenharmony_ci    'lines': ':lines',
1577db96d56Sopenharmony_ci}
1587db96d56Sopenharmony_ci
1597db96d56Sopenharmony_ci# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
1607db96d56Sopenharmony_ci_CRLF = b'\r\n'
1617db96d56Sopenharmony_ci
1627db96d56Sopenharmony_ciGroupInfo = collections.namedtuple('GroupInfo',
1637db96d56Sopenharmony_ci                                   ['group', 'last', 'first', 'flag'])
1647db96d56Sopenharmony_ci
1657db96d56Sopenharmony_ciArticleInfo = collections.namedtuple('ArticleInfo',
1667db96d56Sopenharmony_ci                                     ['number', 'message_id', 'lines'])
1677db96d56Sopenharmony_ci
1687db96d56Sopenharmony_ci
1697db96d56Sopenharmony_ci# Helper function(s)
1707db96d56Sopenharmony_cidef decode_header(header_str):
1717db96d56Sopenharmony_ci    """Takes a unicode string representing a munged header value
1727db96d56Sopenharmony_ci    and decodes it as a (possibly non-ASCII) readable value."""
1737db96d56Sopenharmony_ci    parts = []
1747db96d56Sopenharmony_ci    for v, enc in _email_decode_header(header_str):
1757db96d56Sopenharmony_ci        if isinstance(v, bytes):
1767db96d56Sopenharmony_ci            parts.append(v.decode(enc or 'ascii'))
1777db96d56Sopenharmony_ci        else:
1787db96d56Sopenharmony_ci            parts.append(v)
1797db96d56Sopenharmony_ci    return ''.join(parts)
1807db96d56Sopenharmony_ci
1817db96d56Sopenharmony_cidef _parse_overview_fmt(lines):
1827db96d56Sopenharmony_ci    """Parse a list of string representing the response to LIST OVERVIEW.FMT
1837db96d56Sopenharmony_ci    and return a list of header/metadata names.
1847db96d56Sopenharmony_ci    Raises NNTPDataError if the response is not compliant
1857db96d56Sopenharmony_ci    (cf. RFC 3977, section 8.4)."""
1867db96d56Sopenharmony_ci    fmt = []
1877db96d56Sopenharmony_ci    for line in lines:
1887db96d56Sopenharmony_ci        if line[0] == ':':
1897db96d56Sopenharmony_ci            # Metadata name (e.g. ":bytes")
1907db96d56Sopenharmony_ci            name, _, suffix = line[1:].partition(':')
1917db96d56Sopenharmony_ci            name = ':' + name
1927db96d56Sopenharmony_ci        else:
1937db96d56Sopenharmony_ci            # Header name (e.g. "Subject:" or "Xref:full")
1947db96d56Sopenharmony_ci            name, _, suffix = line.partition(':')
1957db96d56Sopenharmony_ci        name = name.lower()
1967db96d56Sopenharmony_ci        name = _OVERVIEW_FMT_ALTERNATIVES.get(name, name)
1977db96d56Sopenharmony_ci        # Should we do something with the suffix?
1987db96d56Sopenharmony_ci        fmt.append(name)
1997db96d56Sopenharmony_ci    defaults = _DEFAULT_OVERVIEW_FMT
2007db96d56Sopenharmony_ci    if len(fmt) < len(defaults):
2017db96d56Sopenharmony_ci        raise NNTPDataError("LIST OVERVIEW.FMT response too short")
2027db96d56Sopenharmony_ci    if fmt[:len(defaults)] != defaults:
2037db96d56Sopenharmony_ci        raise NNTPDataError("LIST OVERVIEW.FMT redefines default fields")
2047db96d56Sopenharmony_ci    return fmt
2057db96d56Sopenharmony_ci
2067db96d56Sopenharmony_cidef _parse_overview(lines, fmt, data_process_func=None):
2077db96d56Sopenharmony_ci    """Parse the response to an OVER or XOVER command according to the
2087db96d56Sopenharmony_ci    overview format `fmt`."""
2097db96d56Sopenharmony_ci    n_defaults = len(_DEFAULT_OVERVIEW_FMT)
2107db96d56Sopenharmony_ci    overview = []
2117db96d56Sopenharmony_ci    for line in lines:
2127db96d56Sopenharmony_ci        fields = {}
2137db96d56Sopenharmony_ci        article_number, *tokens = line.split('\t')
2147db96d56Sopenharmony_ci        article_number = int(article_number)
2157db96d56Sopenharmony_ci        for i, token in enumerate(tokens):
2167db96d56Sopenharmony_ci            if i >= len(fmt):
2177db96d56Sopenharmony_ci                # XXX should we raise an error? Some servers might not
2187db96d56Sopenharmony_ci                # support LIST OVERVIEW.FMT and still return additional
2197db96d56Sopenharmony_ci                # headers.
2207db96d56Sopenharmony_ci                continue
2217db96d56Sopenharmony_ci            field_name = fmt[i]
2227db96d56Sopenharmony_ci            is_metadata = field_name.startswith(':')
2237db96d56Sopenharmony_ci            if i >= n_defaults and not is_metadata:
2247db96d56Sopenharmony_ci                # Non-default header names are included in full in the response
2257db96d56Sopenharmony_ci                # (unless the field is totally empty)
2267db96d56Sopenharmony_ci                h = field_name + ": "
2277db96d56Sopenharmony_ci                if token and token[:len(h)].lower() != h:
2287db96d56Sopenharmony_ci                    raise NNTPDataError("OVER/XOVER response doesn't include "
2297db96d56Sopenharmony_ci                                        "names of additional headers")
2307db96d56Sopenharmony_ci                token = token[len(h):] if token else None
2317db96d56Sopenharmony_ci            fields[fmt[i]] = token
2327db96d56Sopenharmony_ci        overview.append((article_number, fields))
2337db96d56Sopenharmony_ci    return overview
2347db96d56Sopenharmony_ci
2357db96d56Sopenharmony_cidef _parse_datetime(date_str, time_str=None):
2367db96d56Sopenharmony_ci    """Parse a pair of (date, time) strings, and return a datetime object.
2377db96d56Sopenharmony_ci    If only the date is given, it is assumed to be date and time
2387db96d56Sopenharmony_ci    concatenated together (e.g. response to the DATE command).
2397db96d56Sopenharmony_ci    """
2407db96d56Sopenharmony_ci    if time_str is None:
2417db96d56Sopenharmony_ci        time_str = date_str[-6:]
2427db96d56Sopenharmony_ci        date_str = date_str[:-6]
2437db96d56Sopenharmony_ci    hours = int(time_str[:2])
2447db96d56Sopenharmony_ci    minutes = int(time_str[2:4])
2457db96d56Sopenharmony_ci    seconds = int(time_str[4:])
2467db96d56Sopenharmony_ci    year = int(date_str[:-4])
2477db96d56Sopenharmony_ci    month = int(date_str[-4:-2])
2487db96d56Sopenharmony_ci    day = int(date_str[-2:])
2497db96d56Sopenharmony_ci    # RFC 3977 doesn't say how to interpret 2-char years.  Assume that
2507db96d56Sopenharmony_ci    # there are no dates before 1970 on Usenet.
2517db96d56Sopenharmony_ci    if year < 70:
2527db96d56Sopenharmony_ci        year += 2000
2537db96d56Sopenharmony_ci    elif year < 100:
2547db96d56Sopenharmony_ci        year += 1900
2557db96d56Sopenharmony_ci    return datetime.datetime(year, month, day, hours, minutes, seconds)
2567db96d56Sopenharmony_ci
2577db96d56Sopenharmony_cidef _unparse_datetime(dt, legacy=False):
2587db96d56Sopenharmony_ci    """Format a date or datetime object as a pair of (date, time) strings
2597db96d56Sopenharmony_ci    in the format required by the NEWNEWS and NEWGROUPS commands.  If a
2607db96d56Sopenharmony_ci    date object is passed, the time is assumed to be midnight (00h00).
2617db96d56Sopenharmony_ci
2627db96d56Sopenharmony_ci    The returned representation depends on the legacy flag:
2637db96d56Sopenharmony_ci    * if legacy is False (the default):
2647db96d56Sopenharmony_ci      date has the YYYYMMDD format and time the HHMMSS format
2657db96d56Sopenharmony_ci    * if legacy is True:
2667db96d56Sopenharmony_ci      date has the YYMMDD format and time the HHMMSS format.
2677db96d56Sopenharmony_ci    RFC 3977 compliant servers should understand both formats; therefore,
2687db96d56Sopenharmony_ci    legacy is only needed when talking to old servers.
2697db96d56Sopenharmony_ci    """
2707db96d56Sopenharmony_ci    if not isinstance(dt, datetime.datetime):
2717db96d56Sopenharmony_ci        time_str = "000000"
2727db96d56Sopenharmony_ci    else:
2737db96d56Sopenharmony_ci        time_str = "{0.hour:02d}{0.minute:02d}{0.second:02d}".format(dt)
2747db96d56Sopenharmony_ci    y = dt.year
2757db96d56Sopenharmony_ci    if legacy:
2767db96d56Sopenharmony_ci        y = y % 100
2777db96d56Sopenharmony_ci        date_str = "{0:02d}{1.month:02d}{1.day:02d}".format(y, dt)
2787db96d56Sopenharmony_ci    else:
2797db96d56Sopenharmony_ci        date_str = "{0:04d}{1.month:02d}{1.day:02d}".format(y, dt)
2807db96d56Sopenharmony_ci    return date_str, time_str
2817db96d56Sopenharmony_ci
2827db96d56Sopenharmony_ci
2837db96d56Sopenharmony_ciif _have_ssl:
2847db96d56Sopenharmony_ci
2857db96d56Sopenharmony_ci    def _encrypt_on(sock, context, hostname):
2867db96d56Sopenharmony_ci        """Wrap a socket in SSL/TLS. Arguments:
2877db96d56Sopenharmony_ci        - sock: Socket to wrap
2887db96d56Sopenharmony_ci        - context: SSL context to use for the encrypted connection
2897db96d56Sopenharmony_ci        Returns:
2907db96d56Sopenharmony_ci        - sock: New, encrypted socket.
2917db96d56Sopenharmony_ci        """
2927db96d56Sopenharmony_ci        # Generate a default SSL context if none was passed.
2937db96d56Sopenharmony_ci        if context is None:
2947db96d56Sopenharmony_ci            context = ssl._create_stdlib_context()
2957db96d56Sopenharmony_ci        return context.wrap_socket(sock, server_hostname=hostname)
2967db96d56Sopenharmony_ci
2977db96d56Sopenharmony_ci
2987db96d56Sopenharmony_ci# The classes themselves
2997db96d56Sopenharmony_ciclass NNTP:
3007db96d56Sopenharmony_ci    # UTF-8 is the character set for all NNTP commands and responses: they
3017db96d56Sopenharmony_ci    # are automatically encoded (when sending) and decoded (and receiving)
3027db96d56Sopenharmony_ci    # by this class.
3037db96d56Sopenharmony_ci    # However, some multi-line data blocks can contain arbitrary bytes (for
3047db96d56Sopenharmony_ci    # example, latin-1 or utf-16 data in the body of a message). Commands
3057db96d56Sopenharmony_ci    # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
3067db96d56Sopenharmony_ci    # data will therefore only accept and produce bytes objects.
3077db96d56Sopenharmony_ci    # Furthermore, since there could be non-compliant servers out there,
3087db96d56Sopenharmony_ci    # we use 'surrogateescape' as the error handler for fault tolerance
3097db96d56Sopenharmony_ci    # and easy round-tripping. This could be useful for some applications
3107db96d56Sopenharmony_ci    # (e.g. NNTP gateways).
3117db96d56Sopenharmony_ci
3127db96d56Sopenharmony_ci    encoding = 'utf-8'
3137db96d56Sopenharmony_ci    errors = 'surrogateescape'
3147db96d56Sopenharmony_ci
3157db96d56Sopenharmony_ci    def __init__(self, host, port=NNTP_PORT, user=None, password=None,
3167db96d56Sopenharmony_ci                 readermode=None, usenetrc=False,
3177db96d56Sopenharmony_ci                 timeout=_GLOBAL_DEFAULT_TIMEOUT):
3187db96d56Sopenharmony_ci        """Initialize an instance.  Arguments:
3197db96d56Sopenharmony_ci        - host: hostname to connect to
3207db96d56Sopenharmony_ci        - port: port to connect to (default the standard NNTP port)
3217db96d56Sopenharmony_ci        - user: username to authenticate with
3227db96d56Sopenharmony_ci        - password: password to use with username
3237db96d56Sopenharmony_ci        - readermode: if true, send 'mode reader' command after
3247db96d56Sopenharmony_ci                      connecting.
3257db96d56Sopenharmony_ci        - usenetrc: allow loading username and password from ~/.netrc file
3267db96d56Sopenharmony_ci                    if not specified explicitly
3277db96d56Sopenharmony_ci        - timeout: timeout (in seconds) used for socket connections
3287db96d56Sopenharmony_ci
3297db96d56Sopenharmony_ci        readermode is sometimes necessary if you are connecting to an
3307db96d56Sopenharmony_ci        NNTP server on the local machine and intend to call
3317db96d56Sopenharmony_ci        reader-specific commands, such as `group'.  If you get
3327db96d56Sopenharmony_ci        unexpected NNTPPermanentErrors, you might need to set
3337db96d56Sopenharmony_ci        readermode.
3347db96d56Sopenharmony_ci        """
3357db96d56Sopenharmony_ci        self.host = host
3367db96d56Sopenharmony_ci        self.port = port
3377db96d56Sopenharmony_ci        self.sock = self._create_socket(timeout)
3387db96d56Sopenharmony_ci        self.file = None
3397db96d56Sopenharmony_ci        try:
3407db96d56Sopenharmony_ci            self.file = self.sock.makefile("rwb")
3417db96d56Sopenharmony_ci            self._base_init(readermode)
3427db96d56Sopenharmony_ci            if user or usenetrc:
3437db96d56Sopenharmony_ci                self.login(user, password, usenetrc)
3447db96d56Sopenharmony_ci        except:
3457db96d56Sopenharmony_ci            if self.file:
3467db96d56Sopenharmony_ci                self.file.close()
3477db96d56Sopenharmony_ci            self.sock.close()
3487db96d56Sopenharmony_ci            raise
3497db96d56Sopenharmony_ci
3507db96d56Sopenharmony_ci    def _base_init(self, readermode):
3517db96d56Sopenharmony_ci        """Partial initialization for the NNTP protocol.
3527db96d56Sopenharmony_ci        This instance method is extracted for supporting the test code.
3537db96d56Sopenharmony_ci        """
3547db96d56Sopenharmony_ci        self.debugging = 0
3557db96d56Sopenharmony_ci        self.welcome = self._getresp()
3567db96d56Sopenharmony_ci
3577db96d56Sopenharmony_ci        # Inquire about capabilities (RFC 3977).
3587db96d56Sopenharmony_ci        self._caps = None
3597db96d56Sopenharmony_ci        self.getcapabilities()
3607db96d56Sopenharmony_ci
3617db96d56Sopenharmony_ci        # 'MODE READER' is sometimes necessary to enable 'reader' mode.
3627db96d56Sopenharmony_ci        # However, the order in which 'MODE READER' and 'AUTHINFO' need to
3637db96d56Sopenharmony_ci        # arrive differs between some NNTP servers. If _setreadermode() fails
3647db96d56Sopenharmony_ci        # with an authorization failed error, it will set this to True;
3657db96d56Sopenharmony_ci        # the login() routine will interpret that as a request to try again
3667db96d56Sopenharmony_ci        # after performing its normal function.
3677db96d56Sopenharmony_ci        # Enable only if we're not already in READER mode anyway.
3687db96d56Sopenharmony_ci        self.readermode_afterauth = False
3697db96d56Sopenharmony_ci        if readermode and 'READER' not in self._caps:
3707db96d56Sopenharmony_ci            self._setreadermode()
3717db96d56Sopenharmony_ci            if not self.readermode_afterauth:
3727db96d56Sopenharmony_ci                # Capabilities might have changed after MODE READER
3737db96d56Sopenharmony_ci                self._caps = None
3747db96d56Sopenharmony_ci                self.getcapabilities()
3757db96d56Sopenharmony_ci
3767db96d56Sopenharmony_ci        # RFC 4642 2.2.2: Both the client and the server MUST know if there is
3777db96d56Sopenharmony_ci        # a TLS session active.  A client MUST NOT attempt to start a TLS
3787db96d56Sopenharmony_ci        # session if a TLS session is already active.
3797db96d56Sopenharmony_ci        self.tls_on = False
3807db96d56Sopenharmony_ci
3817db96d56Sopenharmony_ci        # Log in and encryption setup order is left to subclasses.
3827db96d56Sopenharmony_ci        self.authenticated = False
3837db96d56Sopenharmony_ci
3847db96d56Sopenharmony_ci    def __enter__(self):
3857db96d56Sopenharmony_ci        return self
3867db96d56Sopenharmony_ci
3877db96d56Sopenharmony_ci    def __exit__(self, *args):
3887db96d56Sopenharmony_ci        is_connected = lambda: hasattr(self, "file")
3897db96d56Sopenharmony_ci        if is_connected():
3907db96d56Sopenharmony_ci            try:
3917db96d56Sopenharmony_ci                self.quit()
3927db96d56Sopenharmony_ci            except (OSError, EOFError):
3937db96d56Sopenharmony_ci                pass
3947db96d56Sopenharmony_ci            finally:
3957db96d56Sopenharmony_ci                if is_connected():
3967db96d56Sopenharmony_ci                    self._close()
3977db96d56Sopenharmony_ci
3987db96d56Sopenharmony_ci    def _create_socket(self, timeout):
3997db96d56Sopenharmony_ci        if timeout is not None and not timeout:
4007db96d56Sopenharmony_ci            raise ValueError('Non-blocking socket (timeout=0) is not supported')
4017db96d56Sopenharmony_ci        sys.audit("nntplib.connect", self, self.host, self.port)
4027db96d56Sopenharmony_ci        return socket.create_connection((self.host, self.port), timeout)
4037db96d56Sopenharmony_ci
4047db96d56Sopenharmony_ci    def getwelcome(self):
4057db96d56Sopenharmony_ci        """Get the welcome message from the server
4067db96d56Sopenharmony_ci        (this is read and squirreled away by __init__()).
4077db96d56Sopenharmony_ci        If the response code is 200, posting is allowed;
4087db96d56Sopenharmony_ci        if it 201, posting is not allowed."""
4097db96d56Sopenharmony_ci
4107db96d56Sopenharmony_ci        if self.debugging: print('*welcome*', repr(self.welcome))
4117db96d56Sopenharmony_ci        return self.welcome
4127db96d56Sopenharmony_ci
4137db96d56Sopenharmony_ci    def getcapabilities(self):
4147db96d56Sopenharmony_ci        """Get the server capabilities, as read by __init__().
4157db96d56Sopenharmony_ci        If the CAPABILITIES command is not supported, an empty dict is
4167db96d56Sopenharmony_ci        returned."""
4177db96d56Sopenharmony_ci        if self._caps is None:
4187db96d56Sopenharmony_ci            self.nntp_version = 1
4197db96d56Sopenharmony_ci            self.nntp_implementation = None
4207db96d56Sopenharmony_ci            try:
4217db96d56Sopenharmony_ci                resp, caps = self.capabilities()
4227db96d56Sopenharmony_ci            except (NNTPPermanentError, NNTPTemporaryError):
4237db96d56Sopenharmony_ci                # Server doesn't support capabilities
4247db96d56Sopenharmony_ci                self._caps = {}
4257db96d56Sopenharmony_ci            else:
4267db96d56Sopenharmony_ci                self._caps = caps
4277db96d56Sopenharmony_ci                if 'VERSION' in caps:
4287db96d56Sopenharmony_ci                    # The server can advertise several supported versions,
4297db96d56Sopenharmony_ci                    # choose the highest.
4307db96d56Sopenharmony_ci                    self.nntp_version = max(map(int, caps['VERSION']))
4317db96d56Sopenharmony_ci                if 'IMPLEMENTATION' in caps:
4327db96d56Sopenharmony_ci                    self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
4337db96d56Sopenharmony_ci        return self._caps
4347db96d56Sopenharmony_ci
4357db96d56Sopenharmony_ci    def set_debuglevel(self, level):
4367db96d56Sopenharmony_ci        """Set the debugging level.  Argument 'level' means:
4377db96d56Sopenharmony_ci        0: no debugging output (default)
4387db96d56Sopenharmony_ci        1: print commands and responses but not body text etc.
4397db96d56Sopenharmony_ci        2: also print raw lines read and sent before stripping CR/LF"""
4407db96d56Sopenharmony_ci
4417db96d56Sopenharmony_ci        self.debugging = level
4427db96d56Sopenharmony_ci    debug = set_debuglevel
4437db96d56Sopenharmony_ci
4447db96d56Sopenharmony_ci    def _putline(self, line):
4457db96d56Sopenharmony_ci        """Internal: send one line to the server, appending CRLF.
4467db96d56Sopenharmony_ci        The `line` must be a bytes-like object."""
4477db96d56Sopenharmony_ci        sys.audit("nntplib.putline", self, line)
4487db96d56Sopenharmony_ci        line = line + _CRLF
4497db96d56Sopenharmony_ci        if self.debugging > 1: print('*put*', repr(line))
4507db96d56Sopenharmony_ci        self.file.write(line)
4517db96d56Sopenharmony_ci        self.file.flush()
4527db96d56Sopenharmony_ci
4537db96d56Sopenharmony_ci    def _putcmd(self, line):
4547db96d56Sopenharmony_ci        """Internal: send one command to the server (through _putline()).
4557db96d56Sopenharmony_ci        The `line` must be a unicode string."""
4567db96d56Sopenharmony_ci        if self.debugging: print('*cmd*', repr(line))
4577db96d56Sopenharmony_ci        line = line.encode(self.encoding, self.errors)
4587db96d56Sopenharmony_ci        self._putline(line)
4597db96d56Sopenharmony_ci
4607db96d56Sopenharmony_ci    def _getline(self, strip_crlf=True):
4617db96d56Sopenharmony_ci        """Internal: return one line from the server, stripping _CRLF.
4627db96d56Sopenharmony_ci        Raise EOFError if the connection is closed.
4637db96d56Sopenharmony_ci        Returns a bytes object."""
4647db96d56Sopenharmony_ci        line = self.file.readline(_MAXLINE +1)
4657db96d56Sopenharmony_ci        if len(line) > _MAXLINE:
4667db96d56Sopenharmony_ci            raise NNTPDataError('line too long')
4677db96d56Sopenharmony_ci        if self.debugging > 1:
4687db96d56Sopenharmony_ci            print('*get*', repr(line))
4697db96d56Sopenharmony_ci        if not line: raise EOFError
4707db96d56Sopenharmony_ci        if strip_crlf:
4717db96d56Sopenharmony_ci            if line[-2:] == _CRLF:
4727db96d56Sopenharmony_ci                line = line[:-2]
4737db96d56Sopenharmony_ci            elif line[-1:] in _CRLF:
4747db96d56Sopenharmony_ci                line = line[:-1]
4757db96d56Sopenharmony_ci        return line
4767db96d56Sopenharmony_ci
4777db96d56Sopenharmony_ci    def _getresp(self):
4787db96d56Sopenharmony_ci        """Internal: get a response from the server.
4797db96d56Sopenharmony_ci        Raise various errors if the response indicates an error.
4807db96d56Sopenharmony_ci        Returns a unicode string."""
4817db96d56Sopenharmony_ci        resp = self._getline()
4827db96d56Sopenharmony_ci        if self.debugging: print('*resp*', repr(resp))
4837db96d56Sopenharmony_ci        resp = resp.decode(self.encoding, self.errors)
4847db96d56Sopenharmony_ci        c = resp[:1]
4857db96d56Sopenharmony_ci        if c == '4':
4867db96d56Sopenharmony_ci            raise NNTPTemporaryError(resp)
4877db96d56Sopenharmony_ci        if c == '5':
4887db96d56Sopenharmony_ci            raise NNTPPermanentError(resp)
4897db96d56Sopenharmony_ci        if c not in '123':
4907db96d56Sopenharmony_ci            raise NNTPProtocolError(resp)
4917db96d56Sopenharmony_ci        return resp
4927db96d56Sopenharmony_ci
4937db96d56Sopenharmony_ci    def _getlongresp(self, file=None):
4947db96d56Sopenharmony_ci        """Internal: get a response plus following text from the server.
4957db96d56Sopenharmony_ci        Raise various errors if the response indicates an error.
4967db96d56Sopenharmony_ci
4977db96d56Sopenharmony_ci        Returns a (response, lines) tuple where `response` is a unicode
4987db96d56Sopenharmony_ci        string and `lines` is a list of bytes objects.
4997db96d56Sopenharmony_ci        If `file` is a file-like object, it must be open in binary mode.
5007db96d56Sopenharmony_ci        """
5017db96d56Sopenharmony_ci
5027db96d56Sopenharmony_ci        openedFile = None
5037db96d56Sopenharmony_ci        try:
5047db96d56Sopenharmony_ci            # If a string was passed then open a file with that name
5057db96d56Sopenharmony_ci            if isinstance(file, (str, bytes)):
5067db96d56Sopenharmony_ci                openedFile = file = open(file, "wb")
5077db96d56Sopenharmony_ci
5087db96d56Sopenharmony_ci            resp = self._getresp()
5097db96d56Sopenharmony_ci            if resp[:3] not in _LONGRESP:
5107db96d56Sopenharmony_ci                raise NNTPReplyError(resp)
5117db96d56Sopenharmony_ci
5127db96d56Sopenharmony_ci            lines = []
5137db96d56Sopenharmony_ci            if file is not None:
5147db96d56Sopenharmony_ci                # XXX lines = None instead?
5157db96d56Sopenharmony_ci                terminators = (b'.' + _CRLF, b'.\n')
5167db96d56Sopenharmony_ci                while 1:
5177db96d56Sopenharmony_ci                    line = self._getline(False)
5187db96d56Sopenharmony_ci                    if line in terminators:
5197db96d56Sopenharmony_ci                        break
5207db96d56Sopenharmony_ci                    if line.startswith(b'..'):
5217db96d56Sopenharmony_ci                        line = line[1:]
5227db96d56Sopenharmony_ci                    file.write(line)
5237db96d56Sopenharmony_ci            else:
5247db96d56Sopenharmony_ci                terminator = b'.'
5257db96d56Sopenharmony_ci                while 1:
5267db96d56Sopenharmony_ci                    line = self._getline()
5277db96d56Sopenharmony_ci                    if line == terminator:
5287db96d56Sopenharmony_ci                        break
5297db96d56Sopenharmony_ci                    if line.startswith(b'..'):
5307db96d56Sopenharmony_ci                        line = line[1:]
5317db96d56Sopenharmony_ci                    lines.append(line)
5327db96d56Sopenharmony_ci        finally:
5337db96d56Sopenharmony_ci            # If this method created the file, then it must close it
5347db96d56Sopenharmony_ci            if openedFile:
5357db96d56Sopenharmony_ci                openedFile.close()
5367db96d56Sopenharmony_ci
5377db96d56Sopenharmony_ci        return resp, lines
5387db96d56Sopenharmony_ci
5397db96d56Sopenharmony_ci    def _shortcmd(self, line):
5407db96d56Sopenharmony_ci        """Internal: send a command and get the response.
5417db96d56Sopenharmony_ci        Same return value as _getresp()."""
5427db96d56Sopenharmony_ci        self._putcmd(line)
5437db96d56Sopenharmony_ci        return self._getresp()
5447db96d56Sopenharmony_ci
5457db96d56Sopenharmony_ci    def _longcmd(self, line, file=None):
5467db96d56Sopenharmony_ci        """Internal: send a command and get the response plus following text.
5477db96d56Sopenharmony_ci        Same return value as _getlongresp()."""
5487db96d56Sopenharmony_ci        self._putcmd(line)
5497db96d56Sopenharmony_ci        return self._getlongresp(file)
5507db96d56Sopenharmony_ci
5517db96d56Sopenharmony_ci    def _longcmdstring(self, line, file=None):
5527db96d56Sopenharmony_ci        """Internal: send a command and get the response plus following text.
5537db96d56Sopenharmony_ci        Same as _longcmd() and _getlongresp(), except that the returned `lines`
5547db96d56Sopenharmony_ci        are unicode strings rather than bytes objects.
5557db96d56Sopenharmony_ci        """
5567db96d56Sopenharmony_ci        self._putcmd(line)
5577db96d56Sopenharmony_ci        resp, list = self._getlongresp(file)
5587db96d56Sopenharmony_ci        return resp, [line.decode(self.encoding, self.errors)
5597db96d56Sopenharmony_ci                      for line in list]
5607db96d56Sopenharmony_ci
5617db96d56Sopenharmony_ci    def _getoverviewfmt(self):
5627db96d56Sopenharmony_ci        """Internal: get the overview format. Queries the server if not
5637db96d56Sopenharmony_ci        already done, else returns the cached value."""
5647db96d56Sopenharmony_ci        try:
5657db96d56Sopenharmony_ci            return self._cachedoverviewfmt
5667db96d56Sopenharmony_ci        except AttributeError:
5677db96d56Sopenharmony_ci            pass
5687db96d56Sopenharmony_ci        try:
5697db96d56Sopenharmony_ci            resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
5707db96d56Sopenharmony_ci        except NNTPPermanentError:
5717db96d56Sopenharmony_ci            # Not supported by server?
5727db96d56Sopenharmony_ci            fmt = _DEFAULT_OVERVIEW_FMT[:]
5737db96d56Sopenharmony_ci        else:
5747db96d56Sopenharmony_ci            fmt = _parse_overview_fmt(lines)
5757db96d56Sopenharmony_ci        self._cachedoverviewfmt = fmt
5767db96d56Sopenharmony_ci        return fmt
5777db96d56Sopenharmony_ci
5787db96d56Sopenharmony_ci    def _grouplist(self, lines):
5797db96d56Sopenharmony_ci        # Parse lines into "group last first flag"
5807db96d56Sopenharmony_ci        return [GroupInfo(*line.split()) for line in lines]
5817db96d56Sopenharmony_ci
5827db96d56Sopenharmony_ci    def capabilities(self):
5837db96d56Sopenharmony_ci        """Process a CAPABILITIES command.  Not supported by all servers.
5847db96d56Sopenharmony_ci        Return:
5857db96d56Sopenharmony_ci        - resp: server response if successful
5867db96d56Sopenharmony_ci        - caps: a dictionary mapping capability names to lists of tokens
5877db96d56Sopenharmony_ci        (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
5887db96d56Sopenharmony_ci        """
5897db96d56Sopenharmony_ci        caps = {}
5907db96d56Sopenharmony_ci        resp, lines = self._longcmdstring("CAPABILITIES")
5917db96d56Sopenharmony_ci        for line in lines:
5927db96d56Sopenharmony_ci            name, *tokens = line.split()
5937db96d56Sopenharmony_ci            caps[name] = tokens
5947db96d56Sopenharmony_ci        return resp, caps
5957db96d56Sopenharmony_ci
5967db96d56Sopenharmony_ci    def newgroups(self, date, *, file=None):
5977db96d56Sopenharmony_ci        """Process a NEWGROUPS command.  Arguments:
5987db96d56Sopenharmony_ci        - date: a date or datetime object
5997db96d56Sopenharmony_ci        Return:
6007db96d56Sopenharmony_ci        - resp: server response if successful
6017db96d56Sopenharmony_ci        - list: list of newsgroup names
6027db96d56Sopenharmony_ci        """
6037db96d56Sopenharmony_ci        if not isinstance(date, (datetime.date, datetime.date)):
6047db96d56Sopenharmony_ci            raise TypeError(
6057db96d56Sopenharmony_ci                "the date parameter must be a date or datetime object, "
6067db96d56Sopenharmony_ci                "not '{:40}'".format(date.__class__.__name__))
6077db96d56Sopenharmony_ci        date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
6087db96d56Sopenharmony_ci        cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
6097db96d56Sopenharmony_ci        resp, lines = self._longcmdstring(cmd, file)
6107db96d56Sopenharmony_ci        return resp, self._grouplist(lines)
6117db96d56Sopenharmony_ci
6127db96d56Sopenharmony_ci    def newnews(self, group, date, *, file=None):
6137db96d56Sopenharmony_ci        """Process a NEWNEWS command.  Arguments:
6147db96d56Sopenharmony_ci        - group: group name or '*'
6157db96d56Sopenharmony_ci        - date: a date or datetime object
6167db96d56Sopenharmony_ci        Return:
6177db96d56Sopenharmony_ci        - resp: server response if successful
6187db96d56Sopenharmony_ci        - list: list of message ids
6197db96d56Sopenharmony_ci        """
6207db96d56Sopenharmony_ci        if not isinstance(date, (datetime.date, datetime.date)):
6217db96d56Sopenharmony_ci            raise TypeError(
6227db96d56Sopenharmony_ci                "the date parameter must be a date or datetime object, "
6237db96d56Sopenharmony_ci                "not '{:40}'".format(date.__class__.__name__))
6247db96d56Sopenharmony_ci        date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
6257db96d56Sopenharmony_ci        cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
6267db96d56Sopenharmony_ci        return self._longcmdstring(cmd, file)
6277db96d56Sopenharmony_ci
6287db96d56Sopenharmony_ci    def list(self, group_pattern=None, *, file=None):
6297db96d56Sopenharmony_ci        """Process a LIST or LIST ACTIVE command. Arguments:
6307db96d56Sopenharmony_ci        - group_pattern: a pattern indicating which groups to query
6317db96d56Sopenharmony_ci        - file: Filename string or file object to store the result in
6327db96d56Sopenharmony_ci        Returns:
6337db96d56Sopenharmony_ci        - resp: server response if successful
6347db96d56Sopenharmony_ci        - list: list of (group, last, first, flag) (strings)
6357db96d56Sopenharmony_ci        """
6367db96d56Sopenharmony_ci        if group_pattern is not None:
6377db96d56Sopenharmony_ci            command = 'LIST ACTIVE ' + group_pattern
6387db96d56Sopenharmony_ci        else:
6397db96d56Sopenharmony_ci            command = 'LIST'
6407db96d56Sopenharmony_ci        resp, lines = self._longcmdstring(command, file)
6417db96d56Sopenharmony_ci        return resp, self._grouplist(lines)
6427db96d56Sopenharmony_ci
6437db96d56Sopenharmony_ci    def _getdescriptions(self, group_pattern, return_all):
6447db96d56Sopenharmony_ci        line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
6457db96d56Sopenharmony_ci        # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
6467db96d56Sopenharmony_ci        resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
6477db96d56Sopenharmony_ci        if not resp.startswith('215'):
6487db96d56Sopenharmony_ci            # Now the deprecated XGTITLE.  This either raises an error
6497db96d56Sopenharmony_ci            # or succeeds with the same output structure as LIST
6507db96d56Sopenharmony_ci            # NEWSGROUPS.
6517db96d56Sopenharmony_ci            resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
6527db96d56Sopenharmony_ci        groups = {}
6537db96d56Sopenharmony_ci        for raw_line in lines:
6547db96d56Sopenharmony_ci            match = line_pat.search(raw_line.strip())
6557db96d56Sopenharmony_ci            if match:
6567db96d56Sopenharmony_ci                name, desc = match.group(1, 2)
6577db96d56Sopenharmony_ci                if not return_all:
6587db96d56Sopenharmony_ci                    return desc
6597db96d56Sopenharmony_ci                groups[name] = desc
6607db96d56Sopenharmony_ci        if return_all:
6617db96d56Sopenharmony_ci            return resp, groups
6627db96d56Sopenharmony_ci        else:
6637db96d56Sopenharmony_ci            # Nothing found
6647db96d56Sopenharmony_ci            return ''
6657db96d56Sopenharmony_ci
6667db96d56Sopenharmony_ci    def description(self, group):
6677db96d56Sopenharmony_ci        """Get a description for a single group.  If more than one
6687db96d56Sopenharmony_ci        group matches ('group' is a pattern), return the first.  If no
6697db96d56Sopenharmony_ci        group matches, return an empty string.
6707db96d56Sopenharmony_ci
6717db96d56Sopenharmony_ci        This elides the response code from the server, since it can
6727db96d56Sopenharmony_ci        only be '215' or '285' (for xgtitle) anyway.  If the response
6737db96d56Sopenharmony_ci        code is needed, use the 'descriptions' method.
6747db96d56Sopenharmony_ci
6757db96d56Sopenharmony_ci        NOTE: This neither checks for a wildcard in 'group' nor does
6767db96d56Sopenharmony_ci        it check whether the group actually exists."""
6777db96d56Sopenharmony_ci        return self._getdescriptions(group, False)
6787db96d56Sopenharmony_ci
6797db96d56Sopenharmony_ci    def descriptions(self, group_pattern):
6807db96d56Sopenharmony_ci        """Get descriptions for a range of groups."""
6817db96d56Sopenharmony_ci        return self._getdescriptions(group_pattern, True)
6827db96d56Sopenharmony_ci
6837db96d56Sopenharmony_ci    def group(self, name):
6847db96d56Sopenharmony_ci        """Process a GROUP command.  Argument:
6857db96d56Sopenharmony_ci        - group: the group name
6867db96d56Sopenharmony_ci        Returns:
6877db96d56Sopenharmony_ci        - resp: server response if successful
6887db96d56Sopenharmony_ci        - count: number of articles
6897db96d56Sopenharmony_ci        - first: first article number
6907db96d56Sopenharmony_ci        - last: last article number
6917db96d56Sopenharmony_ci        - name: the group name
6927db96d56Sopenharmony_ci        """
6937db96d56Sopenharmony_ci        resp = self._shortcmd('GROUP ' + name)
6947db96d56Sopenharmony_ci        if not resp.startswith('211'):
6957db96d56Sopenharmony_ci            raise NNTPReplyError(resp)
6967db96d56Sopenharmony_ci        words = resp.split()
6977db96d56Sopenharmony_ci        count = first = last = 0
6987db96d56Sopenharmony_ci        n = len(words)
6997db96d56Sopenharmony_ci        if n > 1:
7007db96d56Sopenharmony_ci            count = words[1]
7017db96d56Sopenharmony_ci            if n > 2:
7027db96d56Sopenharmony_ci                first = words[2]
7037db96d56Sopenharmony_ci                if n > 3:
7047db96d56Sopenharmony_ci                    last = words[3]
7057db96d56Sopenharmony_ci                    if n > 4:
7067db96d56Sopenharmony_ci                        name = words[4].lower()
7077db96d56Sopenharmony_ci        return resp, int(count), int(first), int(last), name
7087db96d56Sopenharmony_ci
7097db96d56Sopenharmony_ci    def help(self, *, file=None):
7107db96d56Sopenharmony_ci        """Process a HELP command. Argument:
7117db96d56Sopenharmony_ci        - file: Filename string or file object to store the result in
7127db96d56Sopenharmony_ci        Returns:
7137db96d56Sopenharmony_ci        - resp: server response if successful
7147db96d56Sopenharmony_ci        - list: list of strings returned by the server in response to the
7157db96d56Sopenharmony_ci                HELP command
7167db96d56Sopenharmony_ci        """
7177db96d56Sopenharmony_ci        return self._longcmdstring('HELP', file)
7187db96d56Sopenharmony_ci
7197db96d56Sopenharmony_ci    def _statparse(self, resp):
7207db96d56Sopenharmony_ci        """Internal: parse the response line of a STAT, NEXT, LAST,
7217db96d56Sopenharmony_ci        ARTICLE, HEAD or BODY command."""
7227db96d56Sopenharmony_ci        if not resp.startswith('22'):
7237db96d56Sopenharmony_ci            raise NNTPReplyError(resp)
7247db96d56Sopenharmony_ci        words = resp.split()
7257db96d56Sopenharmony_ci        art_num = int(words[1])
7267db96d56Sopenharmony_ci        message_id = words[2]
7277db96d56Sopenharmony_ci        return resp, art_num, message_id
7287db96d56Sopenharmony_ci
7297db96d56Sopenharmony_ci    def _statcmd(self, line):
7307db96d56Sopenharmony_ci        """Internal: process a STAT, NEXT or LAST command."""
7317db96d56Sopenharmony_ci        resp = self._shortcmd(line)
7327db96d56Sopenharmony_ci        return self._statparse(resp)
7337db96d56Sopenharmony_ci
7347db96d56Sopenharmony_ci    def stat(self, message_spec=None):
7357db96d56Sopenharmony_ci        """Process a STAT command.  Argument:
7367db96d56Sopenharmony_ci        - message_spec: article number or message id (if not specified,
7377db96d56Sopenharmony_ci          the current article is selected)
7387db96d56Sopenharmony_ci        Returns:
7397db96d56Sopenharmony_ci        - resp: server response if successful
7407db96d56Sopenharmony_ci        - art_num: the article number
7417db96d56Sopenharmony_ci        - message_id: the message id
7427db96d56Sopenharmony_ci        """
7437db96d56Sopenharmony_ci        if message_spec:
7447db96d56Sopenharmony_ci            return self._statcmd('STAT {0}'.format(message_spec))
7457db96d56Sopenharmony_ci        else:
7467db96d56Sopenharmony_ci            return self._statcmd('STAT')
7477db96d56Sopenharmony_ci
7487db96d56Sopenharmony_ci    def next(self):
7497db96d56Sopenharmony_ci        """Process a NEXT command.  No arguments.  Return as for STAT."""
7507db96d56Sopenharmony_ci        return self._statcmd('NEXT')
7517db96d56Sopenharmony_ci
7527db96d56Sopenharmony_ci    def last(self):
7537db96d56Sopenharmony_ci        """Process a LAST command.  No arguments.  Return as for STAT."""
7547db96d56Sopenharmony_ci        return self._statcmd('LAST')
7557db96d56Sopenharmony_ci
7567db96d56Sopenharmony_ci    def _artcmd(self, line, file=None):
7577db96d56Sopenharmony_ci        """Internal: process a HEAD, BODY or ARTICLE command."""
7587db96d56Sopenharmony_ci        resp, lines = self._longcmd(line, file)
7597db96d56Sopenharmony_ci        resp, art_num, message_id = self._statparse(resp)
7607db96d56Sopenharmony_ci        return resp, ArticleInfo(art_num, message_id, lines)
7617db96d56Sopenharmony_ci
7627db96d56Sopenharmony_ci    def head(self, message_spec=None, *, file=None):
7637db96d56Sopenharmony_ci        """Process a HEAD command.  Argument:
7647db96d56Sopenharmony_ci        - message_spec: article number or message id
7657db96d56Sopenharmony_ci        - file: filename string or file object to store the headers in
7667db96d56Sopenharmony_ci        Returns:
7677db96d56Sopenharmony_ci        - resp: server response if successful
7687db96d56Sopenharmony_ci        - ArticleInfo: (article number, message id, list of header lines)
7697db96d56Sopenharmony_ci        """
7707db96d56Sopenharmony_ci        if message_spec is not None:
7717db96d56Sopenharmony_ci            cmd = 'HEAD {0}'.format(message_spec)
7727db96d56Sopenharmony_ci        else:
7737db96d56Sopenharmony_ci            cmd = 'HEAD'
7747db96d56Sopenharmony_ci        return self._artcmd(cmd, file)
7757db96d56Sopenharmony_ci
7767db96d56Sopenharmony_ci    def body(self, message_spec=None, *, file=None):
7777db96d56Sopenharmony_ci        """Process a BODY command.  Argument:
7787db96d56Sopenharmony_ci        - message_spec: article number or message id
7797db96d56Sopenharmony_ci        - file: filename string or file object to store the body in
7807db96d56Sopenharmony_ci        Returns:
7817db96d56Sopenharmony_ci        - resp: server response if successful
7827db96d56Sopenharmony_ci        - ArticleInfo: (article number, message id, list of body lines)
7837db96d56Sopenharmony_ci        """
7847db96d56Sopenharmony_ci        if message_spec is not None:
7857db96d56Sopenharmony_ci            cmd = 'BODY {0}'.format(message_spec)
7867db96d56Sopenharmony_ci        else:
7877db96d56Sopenharmony_ci            cmd = 'BODY'
7887db96d56Sopenharmony_ci        return self._artcmd(cmd, file)
7897db96d56Sopenharmony_ci
7907db96d56Sopenharmony_ci    def article(self, message_spec=None, *, file=None):
7917db96d56Sopenharmony_ci        """Process an ARTICLE command.  Argument:
7927db96d56Sopenharmony_ci        - message_spec: article number or message id
7937db96d56Sopenharmony_ci        - file: filename string or file object to store the article in
7947db96d56Sopenharmony_ci        Returns:
7957db96d56Sopenharmony_ci        - resp: server response if successful
7967db96d56Sopenharmony_ci        - ArticleInfo: (article number, message id, list of article lines)
7977db96d56Sopenharmony_ci        """
7987db96d56Sopenharmony_ci        if message_spec is not None:
7997db96d56Sopenharmony_ci            cmd = 'ARTICLE {0}'.format(message_spec)
8007db96d56Sopenharmony_ci        else:
8017db96d56Sopenharmony_ci            cmd = 'ARTICLE'
8027db96d56Sopenharmony_ci        return self._artcmd(cmd, file)
8037db96d56Sopenharmony_ci
8047db96d56Sopenharmony_ci    def slave(self):
8057db96d56Sopenharmony_ci        """Process a SLAVE command.  Returns:
8067db96d56Sopenharmony_ci        - resp: server response if successful
8077db96d56Sopenharmony_ci        """
8087db96d56Sopenharmony_ci        return self._shortcmd('SLAVE')
8097db96d56Sopenharmony_ci
8107db96d56Sopenharmony_ci    def xhdr(self, hdr, str, *, file=None):
8117db96d56Sopenharmony_ci        """Process an XHDR command (optional server extension).  Arguments:
8127db96d56Sopenharmony_ci        - hdr: the header type (e.g. 'subject')
8137db96d56Sopenharmony_ci        - str: an article nr, a message id, or a range nr1-nr2
8147db96d56Sopenharmony_ci        - file: Filename string or file object to store the result in
8157db96d56Sopenharmony_ci        Returns:
8167db96d56Sopenharmony_ci        - resp: server response if successful
8177db96d56Sopenharmony_ci        - list: list of (nr, value) strings
8187db96d56Sopenharmony_ci        """
8197db96d56Sopenharmony_ci        pat = re.compile('^([0-9]+) ?(.*)\n?')
8207db96d56Sopenharmony_ci        resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
8217db96d56Sopenharmony_ci        def remove_number(line):
8227db96d56Sopenharmony_ci            m = pat.match(line)
8237db96d56Sopenharmony_ci            return m.group(1, 2) if m else line
8247db96d56Sopenharmony_ci        return resp, [remove_number(line) for line in lines]
8257db96d56Sopenharmony_ci
8267db96d56Sopenharmony_ci    def xover(self, start, end, *, file=None):
8277db96d56Sopenharmony_ci        """Process an XOVER command (optional server extension) Arguments:
8287db96d56Sopenharmony_ci        - start: start of range
8297db96d56Sopenharmony_ci        - end: end of range
8307db96d56Sopenharmony_ci        - file: Filename string or file object to store the result in
8317db96d56Sopenharmony_ci        Returns:
8327db96d56Sopenharmony_ci        - resp: server response if successful
8337db96d56Sopenharmony_ci        - list: list of dicts containing the response fields
8347db96d56Sopenharmony_ci        """
8357db96d56Sopenharmony_ci        resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
8367db96d56Sopenharmony_ci                                          file)
8377db96d56Sopenharmony_ci        fmt = self._getoverviewfmt()
8387db96d56Sopenharmony_ci        return resp, _parse_overview(lines, fmt)
8397db96d56Sopenharmony_ci
8407db96d56Sopenharmony_ci    def over(self, message_spec, *, file=None):
8417db96d56Sopenharmony_ci        """Process an OVER command.  If the command isn't supported, fall
8427db96d56Sopenharmony_ci        back to XOVER. Arguments:
8437db96d56Sopenharmony_ci        - message_spec:
8447db96d56Sopenharmony_ci            - either a message id, indicating the article to fetch
8457db96d56Sopenharmony_ci              information about
8467db96d56Sopenharmony_ci            - or a (start, end) tuple, indicating a range of article numbers;
8477db96d56Sopenharmony_ci              if end is None, information up to the newest message will be
8487db96d56Sopenharmony_ci              retrieved
8497db96d56Sopenharmony_ci            - or None, indicating the current article number must be used
8507db96d56Sopenharmony_ci        - file: Filename string or file object to store the result in
8517db96d56Sopenharmony_ci        Returns:
8527db96d56Sopenharmony_ci        - resp: server response if successful
8537db96d56Sopenharmony_ci        - list: list of dicts containing the response fields
8547db96d56Sopenharmony_ci
8557db96d56Sopenharmony_ci        NOTE: the "message id" form isn't supported by XOVER
8567db96d56Sopenharmony_ci        """
8577db96d56Sopenharmony_ci        cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
8587db96d56Sopenharmony_ci        if isinstance(message_spec, (tuple, list)):
8597db96d56Sopenharmony_ci            start, end = message_spec
8607db96d56Sopenharmony_ci            cmd += ' {0}-{1}'.format(start, end or '')
8617db96d56Sopenharmony_ci        elif message_spec is not None:
8627db96d56Sopenharmony_ci            cmd = cmd + ' ' + message_spec
8637db96d56Sopenharmony_ci        resp, lines = self._longcmdstring(cmd, file)
8647db96d56Sopenharmony_ci        fmt = self._getoverviewfmt()
8657db96d56Sopenharmony_ci        return resp, _parse_overview(lines, fmt)
8667db96d56Sopenharmony_ci
8677db96d56Sopenharmony_ci    def date(self):
8687db96d56Sopenharmony_ci        """Process the DATE command.
8697db96d56Sopenharmony_ci        Returns:
8707db96d56Sopenharmony_ci        - resp: server response if successful
8717db96d56Sopenharmony_ci        - date: datetime object
8727db96d56Sopenharmony_ci        """
8737db96d56Sopenharmony_ci        resp = self._shortcmd("DATE")
8747db96d56Sopenharmony_ci        if not resp.startswith('111'):
8757db96d56Sopenharmony_ci            raise NNTPReplyError(resp)
8767db96d56Sopenharmony_ci        elem = resp.split()
8777db96d56Sopenharmony_ci        if len(elem) != 2:
8787db96d56Sopenharmony_ci            raise NNTPDataError(resp)
8797db96d56Sopenharmony_ci        date = elem[1]
8807db96d56Sopenharmony_ci        if len(date) != 14:
8817db96d56Sopenharmony_ci            raise NNTPDataError(resp)
8827db96d56Sopenharmony_ci        return resp, _parse_datetime(date, None)
8837db96d56Sopenharmony_ci
8847db96d56Sopenharmony_ci    def _post(self, command, f):
8857db96d56Sopenharmony_ci        resp = self._shortcmd(command)
8867db96d56Sopenharmony_ci        # Raises a specific exception if posting is not allowed
8877db96d56Sopenharmony_ci        if not resp.startswith('3'):
8887db96d56Sopenharmony_ci            raise NNTPReplyError(resp)
8897db96d56Sopenharmony_ci        if isinstance(f, (bytes, bytearray)):
8907db96d56Sopenharmony_ci            f = f.splitlines()
8917db96d56Sopenharmony_ci        # We don't use _putline() because:
8927db96d56Sopenharmony_ci        # - we don't want additional CRLF if the file or iterable is already
8937db96d56Sopenharmony_ci        #   in the right format
8947db96d56Sopenharmony_ci        # - we don't want a spurious flush() after each line is written
8957db96d56Sopenharmony_ci        for line in f:
8967db96d56Sopenharmony_ci            if not line.endswith(_CRLF):
8977db96d56Sopenharmony_ci                line = line.rstrip(b"\r\n") + _CRLF
8987db96d56Sopenharmony_ci            if line.startswith(b'.'):
8997db96d56Sopenharmony_ci                line = b'.' + line
9007db96d56Sopenharmony_ci            self.file.write(line)
9017db96d56Sopenharmony_ci        self.file.write(b".\r\n")
9027db96d56Sopenharmony_ci        self.file.flush()
9037db96d56Sopenharmony_ci        return self._getresp()
9047db96d56Sopenharmony_ci
9057db96d56Sopenharmony_ci    def post(self, data):
9067db96d56Sopenharmony_ci        """Process a POST command.  Arguments:
9077db96d56Sopenharmony_ci        - data: bytes object, iterable or file containing the article
9087db96d56Sopenharmony_ci        Returns:
9097db96d56Sopenharmony_ci        - resp: server response if successful"""
9107db96d56Sopenharmony_ci        return self._post('POST', data)
9117db96d56Sopenharmony_ci
9127db96d56Sopenharmony_ci    def ihave(self, message_id, data):
9137db96d56Sopenharmony_ci        """Process an IHAVE command.  Arguments:
9147db96d56Sopenharmony_ci        - message_id: message-id of the article
9157db96d56Sopenharmony_ci        - data: file containing the article
9167db96d56Sopenharmony_ci        Returns:
9177db96d56Sopenharmony_ci        - resp: server response if successful
9187db96d56Sopenharmony_ci        Note that if the server refuses the article an exception is raised."""
9197db96d56Sopenharmony_ci        return self._post('IHAVE {0}'.format(message_id), data)
9207db96d56Sopenharmony_ci
9217db96d56Sopenharmony_ci    def _close(self):
9227db96d56Sopenharmony_ci        try:
9237db96d56Sopenharmony_ci            if self.file:
9247db96d56Sopenharmony_ci                self.file.close()
9257db96d56Sopenharmony_ci                del self.file
9267db96d56Sopenharmony_ci        finally:
9277db96d56Sopenharmony_ci            self.sock.close()
9287db96d56Sopenharmony_ci
9297db96d56Sopenharmony_ci    def quit(self):
9307db96d56Sopenharmony_ci        """Process a QUIT command and close the socket.  Returns:
9317db96d56Sopenharmony_ci        - resp: server response if successful"""
9327db96d56Sopenharmony_ci        try:
9337db96d56Sopenharmony_ci            resp = self._shortcmd('QUIT')
9347db96d56Sopenharmony_ci        finally:
9357db96d56Sopenharmony_ci            self._close()
9367db96d56Sopenharmony_ci        return resp
9377db96d56Sopenharmony_ci
9387db96d56Sopenharmony_ci    def login(self, user=None, password=None, usenetrc=True):
9397db96d56Sopenharmony_ci        if self.authenticated:
9407db96d56Sopenharmony_ci            raise ValueError("Already logged in.")
9417db96d56Sopenharmony_ci        if not user and not usenetrc:
9427db96d56Sopenharmony_ci            raise ValueError(
9437db96d56Sopenharmony_ci                "At least one of `user` and `usenetrc` must be specified")
9447db96d56Sopenharmony_ci        # If no login/password was specified but netrc was requested,
9457db96d56Sopenharmony_ci        # try to get them from ~/.netrc
9467db96d56Sopenharmony_ci        # Presume that if .netrc has an entry, NNRP authentication is required.
9477db96d56Sopenharmony_ci        try:
9487db96d56Sopenharmony_ci            if usenetrc and not user:
9497db96d56Sopenharmony_ci                import netrc
9507db96d56Sopenharmony_ci                credentials = netrc.netrc()
9517db96d56Sopenharmony_ci                auth = credentials.authenticators(self.host)
9527db96d56Sopenharmony_ci                if auth:
9537db96d56Sopenharmony_ci                    user = auth[0]
9547db96d56Sopenharmony_ci                    password = auth[2]
9557db96d56Sopenharmony_ci        except OSError:
9567db96d56Sopenharmony_ci            pass
9577db96d56Sopenharmony_ci        # Perform NNTP authentication if needed.
9587db96d56Sopenharmony_ci        if not user:
9597db96d56Sopenharmony_ci            return
9607db96d56Sopenharmony_ci        resp = self._shortcmd('authinfo user ' + user)
9617db96d56Sopenharmony_ci        if resp.startswith('381'):
9627db96d56Sopenharmony_ci            if not password:
9637db96d56Sopenharmony_ci                raise NNTPReplyError(resp)
9647db96d56Sopenharmony_ci            else:
9657db96d56Sopenharmony_ci                resp = self._shortcmd('authinfo pass ' + password)
9667db96d56Sopenharmony_ci                if not resp.startswith('281'):
9677db96d56Sopenharmony_ci                    raise NNTPPermanentError(resp)
9687db96d56Sopenharmony_ci        # Capabilities might have changed after login
9697db96d56Sopenharmony_ci        self._caps = None
9707db96d56Sopenharmony_ci        self.getcapabilities()
9717db96d56Sopenharmony_ci        # Attempt to send mode reader if it was requested after login.
9727db96d56Sopenharmony_ci        # Only do so if we're not in reader mode already.
9737db96d56Sopenharmony_ci        if self.readermode_afterauth and 'READER' not in self._caps:
9747db96d56Sopenharmony_ci            self._setreadermode()
9757db96d56Sopenharmony_ci            # Capabilities might have changed after MODE READER
9767db96d56Sopenharmony_ci            self._caps = None
9777db96d56Sopenharmony_ci            self.getcapabilities()
9787db96d56Sopenharmony_ci
9797db96d56Sopenharmony_ci    def _setreadermode(self):
9807db96d56Sopenharmony_ci        try:
9817db96d56Sopenharmony_ci            self.welcome = self._shortcmd('mode reader')
9827db96d56Sopenharmony_ci        except NNTPPermanentError:
9837db96d56Sopenharmony_ci            # Error 5xx, probably 'not implemented'
9847db96d56Sopenharmony_ci            pass
9857db96d56Sopenharmony_ci        except NNTPTemporaryError as e:
9867db96d56Sopenharmony_ci            if e.response.startswith('480'):
9877db96d56Sopenharmony_ci                # Need authorization before 'mode reader'
9887db96d56Sopenharmony_ci                self.readermode_afterauth = True
9897db96d56Sopenharmony_ci            else:
9907db96d56Sopenharmony_ci                raise
9917db96d56Sopenharmony_ci
9927db96d56Sopenharmony_ci    if _have_ssl:
9937db96d56Sopenharmony_ci        def starttls(self, context=None):
9947db96d56Sopenharmony_ci            """Process a STARTTLS command. Arguments:
9957db96d56Sopenharmony_ci            - context: SSL context to use for the encrypted connection
9967db96d56Sopenharmony_ci            """
9977db96d56Sopenharmony_ci            # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if
9987db96d56Sopenharmony_ci            # a TLS session already exists.
9997db96d56Sopenharmony_ci            if self.tls_on:
10007db96d56Sopenharmony_ci                raise ValueError("TLS is already enabled.")
10017db96d56Sopenharmony_ci            if self.authenticated:
10027db96d56Sopenharmony_ci                raise ValueError("TLS cannot be started after authentication.")
10037db96d56Sopenharmony_ci            resp = self._shortcmd('STARTTLS')
10047db96d56Sopenharmony_ci            if resp.startswith('382'):
10057db96d56Sopenharmony_ci                self.file.close()
10067db96d56Sopenharmony_ci                self.sock = _encrypt_on(self.sock, context, self.host)
10077db96d56Sopenharmony_ci                self.file = self.sock.makefile("rwb")
10087db96d56Sopenharmony_ci                self.tls_on = True
10097db96d56Sopenharmony_ci                # Capabilities may change after TLS starts up, so ask for them
10107db96d56Sopenharmony_ci                # again.
10117db96d56Sopenharmony_ci                self._caps = None
10127db96d56Sopenharmony_ci                self.getcapabilities()
10137db96d56Sopenharmony_ci            else:
10147db96d56Sopenharmony_ci                raise NNTPError("TLS failed to start.")
10157db96d56Sopenharmony_ci
10167db96d56Sopenharmony_ci
10177db96d56Sopenharmony_ciif _have_ssl:
10187db96d56Sopenharmony_ci    class NNTP_SSL(NNTP):
10197db96d56Sopenharmony_ci
10207db96d56Sopenharmony_ci        def __init__(self, host, port=NNTP_SSL_PORT,
10217db96d56Sopenharmony_ci                    user=None, password=None, ssl_context=None,
10227db96d56Sopenharmony_ci                    readermode=None, usenetrc=False,
10237db96d56Sopenharmony_ci                    timeout=_GLOBAL_DEFAULT_TIMEOUT):
10247db96d56Sopenharmony_ci            """This works identically to NNTP.__init__, except for the change
10257db96d56Sopenharmony_ci            in default port and the `ssl_context` argument for SSL connections.
10267db96d56Sopenharmony_ci            """
10277db96d56Sopenharmony_ci            self.ssl_context = ssl_context
10287db96d56Sopenharmony_ci            super().__init__(host, port, user, password, readermode,
10297db96d56Sopenharmony_ci                             usenetrc, timeout)
10307db96d56Sopenharmony_ci
10317db96d56Sopenharmony_ci        def _create_socket(self, timeout):
10327db96d56Sopenharmony_ci            sock = super()._create_socket(timeout)
10337db96d56Sopenharmony_ci            try:
10347db96d56Sopenharmony_ci                sock = _encrypt_on(sock, self.ssl_context, self.host)
10357db96d56Sopenharmony_ci            except:
10367db96d56Sopenharmony_ci                sock.close()
10377db96d56Sopenharmony_ci                raise
10387db96d56Sopenharmony_ci            else:
10397db96d56Sopenharmony_ci                return sock
10407db96d56Sopenharmony_ci
10417db96d56Sopenharmony_ci    __all__.append("NNTP_SSL")
10427db96d56Sopenharmony_ci
10437db96d56Sopenharmony_ci
10447db96d56Sopenharmony_ci# Test retrieval when run as a script.
10457db96d56Sopenharmony_ciif __name__ == '__main__':
10467db96d56Sopenharmony_ci    import argparse
10477db96d56Sopenharmony_ci
10487db96d56Sopenharmony_ci    parser = argparse.ArgumentParser(description="""\
10497db96d56Sopenharmony_ci        nntplib built-in demo - display the latest articles in a newsgroup""")
10507db96d56Sopenharmony_ci    parser.add_argument('-g', '--group', default='gmane.comp.python.general',
10517db96d56Sopenharmony_ci                        help='group to fetch messages from (default: %(default)s)')
10527db96d56Sopenharmony_ci    parser.add_argument('-s', '--server', default='news.gmane.io',
10537db96d56Sopenharmony_ci                        help='NNTP server hostname (default: %(default)s)')
10547db96d56Sopenharmony_ci    parser.add_argument('-p', '--port', default=-1, type=int,
10557db96d56Sopenharmony_ci                        help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT))
10567db96d56Sopenharmony_ci    parser.add_argument('-n', '--nb-articles', default=10, type=int,
10577db96d56Sopenharmony_ci                        help='number of articles to fetch (default: %(default)s)')
10587db96d56Sopenharmony_ci    parser.add_argument('-S', '--ssl', action='store_true', default=False,
10597db96d56Sopenharmony_ci                        help='use NNTP over SSL')
10607db96d56Sopenharmony_ci    args = parser.parse_args()
10617db96d56Sopenharmony_ci
10627db96d56Sopenharmony_ci    port = args.port
10637db96d56Sopenharmony_ci    if not args.ssl:
10647db96d56Sopenharmony_ci        if port == -1:
10657db96d56Sopenharmony_ci            port = NNTP_PORT
10667db96d56Sopenharmony_ci        s = NNTP(host=args.server, port=port)
10677db96d56Sopenharmony_ci    else:
10687db96d56Sopenharmony_ci        if port == -1:
10697db96d56Sopenharmony_ci            port = NNTP_SSL_PORT
10707db96d56Sopenharmony_ci        s = NNTP_SSL(host=args.server, port=port)
10717db96d56Sopenharmony_ci
10727db96d56Sopenharmony_ci    caps = s.getcapabilities()
10737db96d56Sopenharmony_ci    if 'STARTTLS' in caps:
10747db96d56Sopenharmony_ci        s.starttls()
10757db96d56Sopenharmony_ci    resp, count, first, last, name = s.group(args.group)
10767db96d56Sopenharmony_ci    print('Group', name, 'has', count, 'articles, range', first, 'to', last)
10777db96d56Sopenharmony_ci
10787db96d56Sopenharmony_ci    def cut(s, lim):
10797db96d56Sopenharmony_ci        if len(s) > lim:
10807db96d56Sopenharmony_ci            s = s[:lim - 4] + "..."
10817db96d56Sopenharmony_ci        return s
10827db96d56Sopenharmony_ci
10837db96d56Sopenharmony_ci    first = str(int(last) - args.nb_articles + 1)
10847db96d56Sopenharmony_ci    resp, overviews = s.xover(first, last)
10857db96d56Sopenharmony_ci    for artnum, over in overviews:
10867db96d56Sopenharmony_ci        author = decode_header(over['from']).split('<', 1)[0]
10877db96d56Sopenharmony_ci        subject = decode_header(over['subject'])
10887db96d56Sopenharmony_ci        lines = int(over[':lines'])
10897db96d56Sopenharmony_ci        print("{:7} {:20} {:42} ({})".format(
10907db96d56Sopenharmony_ci              artnum, cut(author, 20), cut(subject, 42), lines)
10917db96d56Sopenharmony_ci              )
10927db96d56Sopenharmony_ci
10937db96d56Sopenharmony_ci    s.quit()
1094