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