xref: /aosp_15_r20/prebuilts/build-tools/common/py3-stdlib/nntplib.py (revision cda5da8d549138a6648c5ee6d7a49cf8f4a657be)
1*cda5da8dSAndroid Build Coastguard Worker"""An NNTP client class based on:
2*cda5da8dSAndroid Build Coastguard Worker- RFC 977: Network News Transfer Protocol
3*cda5da8dSAndroid Build Coastguard Worker- RFC 2980: Common NNTP Extensions
4*cda5da8dSAndroid Build Coastguard Worker- RFC 3977: Network News Transfer Protocol (version 2)
5*cda5da8dSAndroid Build Coastguard Worker
6*cda5da8dSAndroid Build Coastguard WorkerExample:
7*cda5da8dSAndroid Build Coastguard Worker
8*cda5da8dSAndroid Build Coastguard Worker>>> from nntplib import NNTP
9*cda5da8dSAndroid Build Coastguard Worker>>> s = NNTP('news')
10*cda5da8dSAndroid Build Coastguard Worker>>> resp, count, first, last, name = s.group('comp.lang.python')
11*cda5da8dSAndroid Build Coastguard Worker>>> print('Group', name, 'has', count, 'articles, range', first, 'to', last)
12*cda5da8dSAndroid Build Coastguard WorkerGroup comp.lang.python has 51 articles, range 5770 to 5821
13*cda5da8dSAndroid Build Coastguard Worker>>> resp, subs = s.xhdr('subject', '{0}-{1}'.format(first, last))
14*cda5da8dSAndroid Build Coastguard Worker>>> resp = s.quit()
15*cda5da8dSAndroid Build Coastguard Worker>>>
16*cda5da8dSAndroid Build Coastguard Worker
17*cda5da8dSAndroid Build Coastguard WorkerHere 'resp' is the server response line.
18*cda5da8dSAndroid Build Coastguard WorkerError responses are turned into exceptions.
19*cda5da8dSAndroid Build Coastguard Worker
20*cda5da8dSAndroid Build Coastguard WorkerTo post an article from a file:
21*cda5da8dSAndroid Build Coastguard Worker>>> f = open(filename, 'rb') # file containing article, including header
22*cda5da8dSAndroid Build Coastguard Worker>>> resp = s.post(f)
23*cda5da8dSAndroid Build Coastguard Worker>>>
24*cda5da8dSAndroid Build Coastguard Worker
25*cda5da8dSAndroid Build Coastguard WorkerFor descriptions of all methods, read the comments in the code below.
26*cda5da8dSAndroid Build Coastguard WorkerNote that all arguments and return values representing article numbers
27*cda5da8dSAndroid Build Coastguard Workerare strings, not numbers, since they are rarely used for calculations.
28*cda5da8dSAndroid Build Coastguard Worker"""
29*cda5da8dSAndroid Build Coastguard Worker
30*cda5da8dSAndroid Build Coastguard Worker# RFC 977 by Brian Kantor and Phil Lapsley.
31*cda5da8dSAndroid Build Coastguard Worker# xover, xgtitle, xpath, date methods by Kevan Heydon
32*cda5da8dSAndroid Build Coastguard Worker
33*cda5da8dSAndroid Build Coastguard Worker# Incompatible changes from the 2.x nntplib:
34*cda5da8dSAndroid Build Coastguard Worker# - all commands are encoded as UTF-8 data (using the "surrogateescape"
35*cda5da8dSAndroid Build Coastguard Worker#   error handler), except for raw message data (POST, IHAVE)
36*cda5da8dSAndroid Build Coastguard Worker# - all responses are decoded as UTF-8 data (using the "surrogateescape"
37*cda5da8dSAndroid Build Coastguard Worker#   error handler), except for raw message data (ARTICLE, HEAD, BODY)
38*cda5da8dSAndroid Build Coastguard Worker# - the `file` argument to various methods is keyword-only
39*cda5da8dSAndroid Build Coastguard Worker#
40*cda5da8dSAndroid Build Coastguard Worker# - NNTP.date() returns a datetime object
41*cda5da8dSAndroid Build Coastguard Worker# - NNTP.newgroups() and NNTP.newnews() take a datetime (or date) object,
42*cda5da8dSAndroid Build Coastguard Worker#   rather than a pair of (date, time) strings.
43*cda5da8dSAndroid Build Coastguard Worker# - NNTP.newgroups() and NNTP.list() return a list of GroupInfo named tuples
44*cda5da8dSAndroid Build Coastguard Worker# - NNTP.descriptions() returns a dict mapping group names to descriptions
45*cda5da8dSAndroid Build Coastguard Worker# - NNTP.xover() returns a list of dicts mapping field names (header or metadata)
46*cda5da8dSAndroid Build Coastguard Worker#   to field values; each dict representing a message overview.
47*cda5da8dSAndroid Build Coastguard Worker# - NNTP.article(), NNTP.head() and NNTP.body() return a (response, ArticleInfo)
48*cda5da8dSAndroid Build Coastguard Worker#   tuple.
49*cda5da8dSAndroid Build Coastguard Worker# - the "internal" methods have been marked private (they now start with
50*cda5da8dSAndroid Build Coastguard Worker#   an underscore)
51*cda5da8dSAndroid Build Coastguard Worker
52*cda5da8dSAndroid Build Coastguard Worker# Other changes from the 2.x/3.1 nntplib:
53*cda5da8dSAndroid Build Coastguard Worker# - automatic querying of capabilities at connect
54*cda5da8dSAndroid Build Coastguard Worker# - New method NNTP.getcapabilities()
55*cda5da8dSAndroid Build Coastguard Worker# - New method NNTP.over()
56*cda5da8dSAndroid Build Coastguard Worker# - New helper function decode_header()
57*cda5da8dSAndroid Build Coastguard Worker# - NNTP.post() and NNTP.ihave() accept file objects, bytes-like objects and
58*cda5da8dSAndroid Build Coastguard Worker#   arbitrary iterables yielding lines.
59*cda5da8dSAndroid Build Coastguard Worker# - An extensive test suite :-)
60*cda5da8dSAndroid Build Coastguard Worker
61*cda5da8dSAndroid Build Coastguard Worker# TODO:
62*cda5da8dSAndroid Build Coastguard Worker# - return structured data (GroupInfo etc.) everywhere
63*cda5da8dSAndroid Build Coastguard Worker# - support HDR
64*cda5da8dSAndroid Build Coastguard Worker
65*cda5da8dSAndroid Build Coastguard Worker# Imports
66*cda5da8dSAndroid Build Coastguard Workerimport re
67*cda5da8dSAndroid Build Coastguard Workerimport socket
68*cda5da8dSAndroid Build Coastguard Workerimport collections
69*cda5da8dSAndroid Build Coastguard Workerimport datetime
70*cda5da8dSAndroid Build Coastguard Workerimport sys
71*cda5da8dSAndroid Build Coastguard Workerimport warnings
72*cda5da8dSAndroid Build Coastguard Worker
73*cda5da8dSAndroid Build Coastguard Workertry:
74*cda5da8dSAndroid Build Coastguard Worker    import ssl
75*cda5da8dSAndroid Build Coastguard Workerexcept ImportError:
76*cda5da8dSAndroid Build Coastguard Worker    _have_ssl = False
77*cda5da8dSAndroid Build Coastguard Workerelse:
78*cda5da8dSAndroid Build Coastguard Worker    _have_ssl = True
79*cda5da8dSAndroid Build Coastguard Worker
80*cda5da8dSAndroid Build Coastguard Workerfrom email.header import decode_header as _email_decode_header
81*cda5da8dSAndroid Build Coastguard Workerfrom socket import _GLOBAL_DEFAULT_TIMEOUT
82*cda5da8dSAndroid Build Coastguard Worker
83*cda5da8dSAndroid Build Coastguard Worker__all__ = ["NNTP",
84*cda5da8dSAndroid Build Coastguard Worker           "NNTPError", "NNTPReplyError", "NNTPTemporaryError",
85*cda5da8dSAndroid Build Coastguard Worker           "NNTPPermanentError", "NNTPProtocolError", "NNTPDataError",
86*cda5da8dSAndroid Build Coastguard Worker           "decode_header",
87*cda5da8dSAndroid Build Coastguard Worker           ]
88*cda5da8dSAndroid Build Coastguard Worker
89*cda5da8dSAndroid Build Coastguard Workerwarnings._deprecated(__name__, remove=(3, 13))
90*cda5da8dSAndroid Build Coastguard Worker
91*cda5da8dSAndroid Build Coastguard Worker# maximal line length when calling readline(). This is to prevent
92*cda5da8dSAndroid Build Coastguard Worker# reading arbitrary length lines. RFC 3977 limits NNTP line length to
93*cda5da8dSAndroid Build Coastguard Worker# 512 characters, including CRLF. We have selected 2048 just to be on
94*cda5da8dSAndroid Build Coastguard Worker# the safe side.
95*cda5da8dSAndroid Build Coastguard Worker_MAXLINE = 2048
96*cda5da8dSAndroid Build Coastguard Worker
97*cda5da8dSAndroid Build Coastguard Worker
98*cda5da8dSAndroid Build Coastguard Worker# Exceptions raised when an error or invalid response is received
99*cda5da8dSAndroid Build Coastguard Workerclass NNTPError(Exception):
100*cda5da8dSAndroid Build Coastguard Worker    """Base class for all nntplib exceptions"""
101*cda5da8dSAndroid Build Coastguard Worker    def __init__(self, *args):
102*cda5da8dSAndroid Build Coastguard Worker        Exception.__init__(self, *args)
103*cda5da8dSAndroid Build Coastguard Worker        try:
104*cda5da8dSAndroid Build Coastguard Worker            self.response = args[0]
105*cda5da8dSAndroid Build Coastguard Worker        except IndexError:
106*cda5da8dSAndroid Build Coastguard Worker            self.response = 'No response given'
107*cda5da8dSAndroid Build Coastguard Worker
108*cda5da8dSAndroid Build Coastguard Workerclass NNTPReplyError(NNTPError):
109*cda5da8dSAndroid Build Coastguard Worker    """Unexpected [123]xx reply"""
110*cda5da8dSAndroid Build Coastguard Worker    pass
111*cda5da8dSAndroid Build Coastguard Worker
112*cda5da8dSAndroid Build Coastguard Workerclass NNTPTemporaryError(NNTPError):
113*cda5da8dSAndroid Build Coastguard Worker    """4xx errors"""
114*cda5da8dSAndroid Build Coastguard Worker    pass
115*cda5da8dSAndroid Build Coastguard Worker
116*cda5da8dSAndroid Build Coastguard Workerclass NNTPPermanentError(NNTPError):
117*cda5da8dSAndroid Build Coastguard Worker    """5xx errors"""
118*cda5da8dSAndroid Build Coastguard Worker    pass
119*cda5da8dSAndroid Build Coastguard Worker
120*cda5da8dSAndroid Build Coastguard Workerclass NNTPProtocolError(NNTPError):
121*cda5da8dSAndroid Build Coastguard Worker    """Response does not begin with [1-5]"""
122*cda5da8dSAndroid Build Coastguard Worker    pass
123*cda5da8dSAndroid Build Coastguard Worker
124*cda5da8dSAndroid Build Coastguard Workerclass NNTPDataError(NNTPError):
125*cda5da8dSAndroid Build Coastguard Worker    """Error in response data"""
126*cda5da8dSAndroid Build Coastguard Worker    pass
127*cda5da8dSAndroid Build Coastguard Worker
128*cda5da8dSAndroid Build Coastguard Worker
129*cda5da8dSAndroid Build Coastguard Worker# Standard port used by NNTP servers
130*cda5da8dSAndroid Build Coastguard WorkerNNTP_PORT = 119
131*cda5da8dSAndroid Build Coastguard WorkerNNTP_SSL_PORT = 563
132*cda5da8dSAndroid Build Coastguard Worker
133*cda5da8dSAndroid Build Coastguard Worker# Response numbers that are followed by additional text (e.g. article)
134*cda5da8dSAndroid Build Coastguard Worker_LONGRESP = {
135*cda5da8dSAndroid Build Coastguard Worker    '100',   # HELP
136*cda5da8dSAndroid Build Coastguard Worker    '101',   # CAPABILITIES
137*cda5da8dSAndroid Build Coastguard Worker    '211',   # LISTGROUP   (also not multi-line with GROUP)
138*cda5da8dSAndroid Build Coastguard Worker    '215',   # LIST
139*cda5da8dSAndroid Build Coastguard Worker    '220',   # ARTICLE
140*cda5da8dSAndroid Build Coastguard Worker    '221',   # HEAD, XHDR
141*cda5da8dSAndroid Build Coastguard Worker    '222',   # BODY
142*cda5da8dSAndroid Build Coastguard Worker    '224',   # OVER, XOVER
143*cda5da8dSAndroid Build Coastguard Worker    '225',   # HDR
144*cda5da8dSAndroid Build Coastguard Worker    '230',   # NEWNEWS
145*cda5da8dSAndroid Build Coastguard Worker    '231',   # NEWGROUPS
146*cda5da8dSAndroid Build Coastguard Worker    '282',   # XGTITLE
147*cda5da8dSAndroid Build Coastguard Worker}
148*cda5da8dSAndroid Build Coastguard Worker
149*cda5da8dSAndroid Build Coastguard Worker# Default decoded value for LIST OVERVIEW.FMT if not supported
150*cda5da8dSAndroid Build Coastguard Worker_DEFAULT_OVERVIEW_FMT = [
151*cda5da8dSAndroid Build Coastguard Worker    "subject", "from", "date", "message-id", "references", ":bytes", ":lines"]
152*cda5da8dSAndroid Build Coastguard Worker
153*cda5da8dSAndroid Build Coastguard Worker# Alternative names allowed in LIST OVERVIEW.FMT response
154*cda5da8dSAndroid Build Coastguard Worker_OVERVIEW_FMT_ALTERNATIVES = {
155*cda5da8dSAndroid Build Coastguard Worker    'bytes': ':bytes',
156*cda5da8dSAndroid Build Coastguard Worker    'lines': ':lines',
157*cda5da8dSAndroid Build Coastguard Worker}
158*cda5da8dSAndroid Build Coastguard Worker
159*cda5da8dSAndroid Build Coastguard Worker# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
160*cda5da8dSAndroid Build Coastguard Worker_CRLF = b'\r\n'
161*cda5da8dSAndroid Build Coastguard Worker
162*cda5da8dSAndroid Build Coastguard WorkerGroupInfo = collections.namedtuple('GroupInfo',
163*cda5da8dSAndroid Build Coastguard Worker                                   ['group', 'last', 'first', 'flag'])
164*cda5da8dSAndroid Build Coastguard Worker
165*cda5da8dSAndroid Build Coastguard WorkerArticleInfo = collections.namedtuple('ArticleInfo',
166*cda5da8dSAndroid Build Coastguard Worker                                     ['number', 'message_id', 'lines'])
167*cda5da8dSAndroid Build Coastguard Worker
168*cda5da8dSAndroid Build Coastguard Worker
169*cda5da8dSAndroid Build Coastguard Worker# Helper function(s)
170*cda5da8dSAndroid Build Coastguard Workerdef decode_header(header_str):
171*cda5da8dSAndroid Build Coastguard Worker    """Takes a unicode string representing a munged header value
172*cda5da8dSAndroid Build Coastguard Worker    and decodes it as a (possibly non-ASCII) readable value."""
173*cda5da8dSAndroid Build Coastguard Worker    parts = []
174*cda5da8dSAndroid Build Coastguard Worker    for v, enc in _email_decode_header(header_str):
175*cda5da8dSAndroid Build Coastguard Worker        if isinstance(v, bytes):
176*cda5da8dSAndroid Build Coastguard Worker            parts.append(v.decode(enc or 'ascii'))
177*cda5da8dSAndroid Build Coastguard Worker        else:
178*cda5da8dSAndroid Build Coastguard Worker            parts.append(v)
179*cda5da8dSAndroid Build Coastguard Worker    return ''.join(parts)
180*cda5da8dSAndroid Build Coastguard Worker
181*cda5da8dSAndroid Build Coastguard Workerdef _parse_overview_fmt(lines):
182*cda5da8dSAndroid Build Coastguard Worker    """Parse a list of string representing the response to LIST OVERVIEW.FMT
183*cda5da8dSAndroid Build Coastguard Worker    and return a list of header/metadata names.
184*cda5da8dSAndroid Build Coastguard Worker    Raises NNTPDataError if the response is not compliant
185*cda5da8dSAndroid Build Coastguard Worker    (cf. RFC 3977, section 8.4)."""
186*cda5da8dSAndroid Build Coastguard Worker    fmt = []
187*cda5da8dSAndroid Build Coastguard Worker    for line in lines:
188*cda5da8dSAndroid Build Coastguard Worker        if line[0] == ':':
189*cda5da8dSAndroid Build Coastguard Worker            # Metadata name (e.g. ":bytes")
190*cda5da8dSAndroid Build Coastguard Worker            name, _, suffix = line[1:].partition(':')
191*cda5da8dSAndroid Build Coastguard Worker            name = ':' + name
192*cda5da8dSAndroid Build Coastguard Worker        else:
193*cda5da8dSAndroid Build Coastguard Worker            # Header name (e.g. "Subject:" or "Xref:full")
194*cda5da8dSAndroid Build Coastguard Worker            name, _, suffix = line.partition(':')
195*cda5da8dSAndroid Build Coastguard Worker        name = name.lower()
196*cda5da8dSAndroid Build Coastguard Worker        name = _OVERVIEW_FMT_ALTERNATIVES.get(name, name)
197*cda5da8dSAndroid Build Coastguard Worker        # Should we do something with the suffix?
198*cda5da8dSAndroid Build Coastguard Worker        fmt.append(name)
199*cda5da8dSAndroid Build Coastguard Worker    defaults = _DEFAULT_OVERVIEW_FMT
200*cda5da8dSAndroid Build Coastguard Worker    if len(fmt) < len(defaults):
201*cda5da8dSAndroid Build Coastguard Worker        raise NNTPDataError("LIST OVERVIEW.FMT response too short")
202*cda5da8dSAndroid Build Coastguard Worker    if fmt[:len(defaults)] != defaults:
203*cda5da8dSAndroid Build Coastguard Worker        raise NNTPDataError("LIST OVERVIEW.FMT redefines default fields")
204*cda5da8dSAndroid Build Coastguard Worker    return fmt
205*cda5da8dSAndroid Build Coastguard Worker
206*cda5da8dSAndroid Build Coastguard Workerdef _parse_overview(lines, fmt, data_process_func=None):
207*cda5da8dSAndroid Build Coastguard Worker    """Parse the response to an OVER or XOVER command according to the
208*cda5da8dSAndroid Build Coastguard Worker    overview format `fmt`."""
209*cda5da8dSAndroid Build Coastguard Worker    n_defaults = len(_DEFAULT_OVERVIEW_FMT)
210*cda5da8dSAndroid Build Coastguard Worker    overview = []
211*cda5da8dSAndroid Build Coastguard Worker    for line in lines:
212*cda5da8dSAndroid Build Coastguard Worker        fields = {}
213*cda5da8dSAndroid Build Coastguard Worker        article_number, *tokens = line.split('\t')
214*cda5da8dSAndroid Build Coastguard Worker        article_number = int(article_number)
215*cda5da8dSAndroid Build Coastguard Worker        for i, token in enumerate(tokens):
216*cda5da8dSAndroid Build Coastguard Worker            if i >= len(fmt):
217*cda5da8dSAndroid Build Coastguard Worker                # XXX should we raise an error? Some servers might not
218*cda5da8dSAndroid Build Coastguard Worker                # support LIST OVERVIEW.FMT and still return additional
219*cda5da8dSAndroid Build Coastguard Worker                # headers.
220*cda5da8dSAndroid Build Coastguard Worker                continue
221*cda5da8dSAndroid Build Coastguard Worker            field_name = fmt[i]
222*cda5da8dSAndroid Build Coastguard Worker            is_metadata = field_name.startswith(':')
223*cda5da8dSAndroid Build Coastguard Worker            if i >= n_defaults and not is_metadata:
224*cda5da8dSAndroid Build Coastguard Worker                # Non-default header names are included in full in the response
225*cda5da8dSAndroid Build Coastguard Worker                # (unless the field is totally empty)
226*cda5da8dSAndroid Build Coastguard Worker                h = field_name + ": "
227*cda5da8dSAndroid Build Coastguard Worker                if token and token[:len(h)].lower() != h:
228*cda5da8dSAndroid Build Coastguard Worker                    raise NNTPDataError("OVER/XOVER response doesn't include "
229*cda5da8dSAndroid Build Coastguard Worker                                        "names of additional headers")
230*cda5da8dSAndroid Build Coastguard Worker                token = token[len(h):] if token else None
231*cda5da8dSAndroid Build Coastguard Worker            fields[fmt[i]] = token
232*cda5da8dSAndroid Build Coastguard Worker        overview.append((article_number, fields))
233*cda5da8dSAndroid Build Coastguard Worker    return overview
234*cda5da8dSAndroid Build Coastguard Worker
235*cda5da8dSAndroid Build Coastguard Workerdef _parse_datetime(date_str, time_str=None):
236*cda5da8dSAndroid Build Coastguard Worker    """Parse a pair of (date, time) strings, and return a datetime object.
237*cda5da8dSAndroid Build Coastguard Worker    If only the date is given, it is assumed to be date and time
238*cda5da8dSAndroid Build Coastguard Worker    concatenated together (e.g. response to the DATE command).
239*cda5da8dSAndroid Build Coastguard Worker    """
240*cda5da8dSAndroid Build Coastguard Worker    if time_str is None:
241*cda5da8dSAndroid Build Coastguard Worker        time_str = date_str[-6:]
242*cda5da8dSAndroid Build Coastguard Worker        date_str = date_str[:-6]
243*cda5da8dSAndroid Build Coastguard Worker    hours = int(time_str[:2])
244*cda5da8dSAndroid Build Coastguard Worker    minutes = int(time_str[2:4])
245*cda5da8dSAndroid Build Coastguard Worker    seconds = int(time_str[4:])
246*cda5da8dSAndroid Build Coastguard Worker    year = int(date_str[:-4])
247*cda5da8dSAndroid Build Coastguard Worker    month = int(date_str[-4:-2])
248*cda5da8dSAndroid Build Coastguard Worker    day = int(date_str[-2:])
249*cda5da8dSAndroid Build Coastguard Worker    # RFC 3977 doesn't say how to interpret 2-char years.  Assume that
250*cda5da8dSAndroid Build Coastguard Worker    # there are no dates before 1970 on Usenet.
251*cda5da8dSAndroid Build Coastguard Worker    if year < 70:
252*cda5da8dSAndroid Build Coastguard Worker        year += 2000
253*cda5da8dSAndroid Build Coastguard Worker    elif year < 100:
254*cda5da8dSAndroid Build Coastguard Worker        year += 1900
255*cda5da8dSAndroid Build Coastguard Worker    return datetime.datetime(year, month, day, hours, minutes, seconds)
256*cda5da8dSAndroid Build Coastguard Worker
257*cda5da8dSAndroid Build Coastguard Workerdef _unparse_datetime(dt, legacy=False):
258*cda5da8dSAndroid Build Coastguard Worker    """Format a date or datetime object as a pair of (date, time) strings
259*cda5da8dSAndroid Build Coastguard Worker    in the format required by the NEWNEWS and NEWGROUPS commands.  If a
260*cda5da8dSAndroid Build Coastguard Worker    date object is passed, the time is assumed to be midnight (00h00).
261*cda5da8dSAndroid Build Coastguard Worker
262*cda5da8dSAndroid Build Coastguard Worker    The returned representation depends on the legacy flag:
263*cda5da8dSAndroid Build Coastguard Worker    * if legacy is False (the default):
264*cda5da8dSAndroid Build Coastguard Worker      date has the YYYYMMDD format and time the HHMMSS format
265*cda5da8dSAndroid Build Coastguard Worker    * if legacy is True:
266*cda5da8dSAndroid Build Coastguard Worker      date has the YYMMDD format and time the HHMMSS format.
267*cda5da8dSAndroid Build Coastguard Worker    RFC 3977 compliant servers should understand both formats; therefore,
268*cda5da8dSAndroid Build Coastguard Worker    legacy is only needed when talking to old servers.
269*cda5da8dSAndroid Build Coastguard Worker    """
270*cda5da8dSAndroid Build Coastguard Worker    if not isinstance(dt, datetime.datetime):
271*cda5da8dSAndroid Build Coastguard Worker        time_str = "000000"
272*cda5da8dSAndroid Build Coastguard Worker    else:
273*cda5da8dSAndroid Build Coastguard Worker        time_str = "{0.hour:02d}{0.minute:02d}{0.second:02d}".format(dt)
274*cda5da8dSAndroid Build Coastguard Worker    y = dt.year
275*cda5da8dSAndroid Build Coastguard Worker    if legacy:
276*cda5da8dSAndroid Build Coastguard Worker        y = y % 100
277*cda5da8dSAndroid Build Coastguard Worker        date_str = "{0:02d}{1.month:02d}{1.day:02d}".format(y, dt)
278*cda5da8dSAndroid Build Coastguard Worker    else:
279*cda5da8dSAndroid Build Coastguard Worker        date_str = "{0:04d}{1.month:02d}{1.day:02d}".format(y, dt)
280*cda5da8dSAndroid Build Coastguard Worker    return date_str, time_str
281*cda5da8dSAndroid Build Coastguard Worker
282*cda5da8dSAndroid Build Coastguard Worker
283*cda5da8dSAndroid Build Coastguard Workerif _have_ssl:
284*cda5da8dSAndroid Build Coastguard Worker
285*cda5da8dSAndroid Build Coastguard Worker    def _encrypt_on(sock, context, hostname):
286*cda5da8dSAndroid Build Coastguard Worker        """Wrap a socket in SSL/TLS. Arguments:
287*cda5da8dSAndroid Build Coastguard Worker        - sock: Socket to wrap
288*cda5da8dSAndroid Build Coastguard Worker        - context: SSL context to use for the encrypted connection
289*cda5da8dSAndroid Build Coastguard Worker        Returns:
290*cda5da8dSAndroid Build Coastguard Worker        - sock: New, encrypted socket.
291*cda5da8dSAndroid Build Coastguard Worker        """
292*cda5da8dSAndroid Build Coastguard Worker        # Generate a default SSL context if none was passed.
293*cda5da8dSAndroid Build Coastguard Worker        if context is None:
294*cda5da8dSAndroid Build Coastguard Worker            context = ssl._create_stdlib_context()
295*cda5da8dSAndroid Build Coastguard Worker        return context.wrap_socket(sock, server_hostname=hostname)
296*cda5da8dSAndroid Build Coastguard Worker
297*cda5da8dSAndroid Build Coastguard Worker
298*cda5da8dSAndroid Build Coastguard Worker# The classes themselves
299*cda5da8dSAndroid Build Coastguard Workerclass NNTP:
300*cda5da8dSAndroid Build Coastguard Worker    # UTF-8 is the character set for all NNTP commands and responses: they
301*cda5da8dSAndroid Build Coastguard Worker    # are automatically encoded (when sending) and decoded (and receiving)
302*cda5da8dSAndroid Build Coastguard Worker    # by this class.
303*cda5da8dSAndroid Build Coastguard Worker    # However, some multi-line data blocks can contain arbitrary bytes (for
304*cda5da8dSAndroid Build Coastguard Worker    # example, latin-1 or utf-16 data in the body of a message). Commands
305*cda5da8dSAndroid Build Coastguard Worker    # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message
306*cda5da8dSAndroid Build Coastguard Worker    # data will therefore only accept and produce bytes objects.
307*cda5da8dSAndroid Build Coastguard Worker    # Furthermore, since there could be non-compliant servers out there,
308*cda5da8dSAndroid Build Coastguard Worker    # we use 'surrogateescape' as the error handler for fault tolerance
309*cda5da8dSAndroid Build Coastguard Worker    # and easy round-tripping. This could be useful for some applications
310*cda5da8dSAndroid Build Coastguard Worker    # (e.g. NNTP gateways).
311*cda5da8dSAndroid Build Coastguard Worker
312*cda5da8dSAndroid Build Coastguard Worker    encoding = 'utf-8'
313*cda5da8dSAndroid Build Coastguard Worker    errors = 'surrogateescape'
314*cda5da8dSAndroid Build Coastguard Worker
315*cda5da8dSAndroid Build Coastguard Worker    def __init__(self, host, port=NNTP_PORT, user=None, password=None,
316*cda5da8dSAndroid Build Coastguard Worker                 readermode=None, usenetrc=False,
317*cda5da8dSAndroid Build Coastguard Worker                 timeout=_GLOBAL_DEFAULT_TIMEOUT):
318*cda5da8dSAndroid Build Coastguard Worker        """Initialize an instance.  Arguments:
319*cda5da8dSAndroid Build Coastguard Worker        - host: hostname to connect to
320*cda5da8dSAndroid Build Coastguard Worker        - port: port to connect to (default the standard NNTP port)
321*cda5da8dSAndroid Build Coastguard Worker        - user: username to authenticate with
322*cda5da8dSAndroid Build Coastguard Worker        - password: password to use with username
323*cda5da8dSAndroid Build Coastguard Worker        - readermode: if true, send 'mode reader' command after
324*cda5da8dSAndroid Build Coastguard Worker                      connecting.
325*cda5da8dSAndroid Build Coastguard Worker        - usenetrc: allow loading username and password from ~/.netrc file
326*cda5da8dSAndroid Build Coastguard Worker                    if not specified explicitly
327*cda5da8dSAndroid Build Coastguard Worker        - timeout: timeout (in seconds) used for socket connections
328*cda5da8dSAndroid Build Coastguard Worker
329*cda5da8dSAndroid Build Coastguard Worker        readermode is sometimes necessary if you are connecting to an
330*cda5da8dSAndroid Build Coastguard Worker        NNTP server on the local machine and intend to call
331*cda5da8dSAndroid Build Coastguard Worker        reader-specific commands, such as `group'.  If you get
332*cda5da8dSAndroid Build Coastguard Worker        unexpected NNTPPermanentErrors, you might need to set
333*cda5da8dSAndroid Build Coastguard Worker        readermode.
334*cda5da8dSAndroid Build Coastguard Worker        """
335*cda5da8dSAndroid Build Coastguard Worker        self.host = host
336*cda5da8dSAndroid Build Coastguard Worker        self.port = port
337*cda5da8dSAndroid Build Coastguard Worker        self.sock = self._create_socket(timeout)
338*cda5da8dSAndroid Build Coastguard Worker        self.file = None
339*cda5da8dSAndroid Build Coastguard Worker        try:
340*cda5da8dSAndroid Build Coastguard Worker            self.file = self.sock.makefile("rwb")
341*cda5da8dSAndroid Build Coastguard Worker            self._base_init(readermode)
342*cda5da8dSAndroid Build Coastguard Worker            if user or usenetrc:
343*cda5da8dSAndroid Build Coastguard Worker                self.login(user, password, usenetrc)
344*cda5da8dSAndroid Build Coastguard Worker        except:
345*cda5da8dSAndroid Build Coastguard Worker            if self.file:
346*cda5da8dSAndroid Build Coastguard Worker                self.file.close()
347*cda5da8dSAndroid Build Coastguard Worker            self.sock.close()
348*cda5da8dSAndroid Build Coastguard Worker            raise
349*cda5da8dSAndroid Build Coastguard Worker
350*cda5da8dSAndroid Build Coastguard Worker    def _base_init(self, readermode):
351*cda5da8dSAndroid Build Coastguard Worker        """Partial initialization for the NNTP protocol.
352*cda5da8dSAndroid Build Coastguard Worker        This instance method is extracted for supporting the test code.
353*cda5da8dSAndroid Build Coastguard Worker        """
354*cda5da8dSAndroid Build Coastguard Worker        self.debugging = 0
355*cda5da8dSAndroid Build Coastguard Worker        self.welcome = self._getresp()
356*cda5da8dSAndroid Build Coastguard Worker
357*cda5da8dSAndroid Build Coastguard Worker        # Inquire about capabilities (RFC 3977).
358*cda5da8dSAndroid Build Coastguard Worker        self._caps = None
359*cda5da8dSAndroid Build Coastguard Worker        self.getcapabilities()
360*cda5da8dSAndroid Build Coastguard Worker
361*cda5da8dSAndroid Build Coastguard Worker        # 'MODE READER' is sometimes necessary to enable 'reader' mode.
362*cda5da8dSAndroid Build Coastguard Worker        # However, the order in which 'MODE READER' and 'AUTHINFO' need to
363*cda5da8dSAndroid Build Coastguard Worker        # arrive differs between some NNTP servers. If _setreadermode() fails
364*cda5da8dSAndroid Build Coastguard Worker        # with an authorization failed error, it will set this to True;
365*cda5da8dSAndroid Build Coastguard Worker        # the login() routine will interpret that as a request to try again
366*cda5da8dSAndroid Build Coastguard Worker        # after performing its normal function.
367*cda5da8dSAndroid Build Coastguard Worker        # Enable only if we're not already in READER mode anyway.
368*cda5da8dSAndroid Build Coastguard Worker        self.readermode_afterauth = False
369*cda5da8dSAndroid Build Coastguard Worker        if readermode and 'READER' not in self._caps:
370*cda5da8dSAndroid Build Coastguard Worker            self._setreadermode()
371*cda5da8dSAndroid Build Coastguard Worker            if not self.readermode_afterauth:
372*cda5da8dSAndroid Build Coastguard Worker                # Capabilities might have changed after MODE READER
373*cda5da8dSAndroid Build Coastguard Worker                self._caps = None
374*cda5da8dSAndroid Build Coastguard Worker                self.getcapabilities()
375*cda5da8dSAndroid Build Coastguard Worker
376*cda5da8dSAndroid Build Coastguard Worker        # RFC 4642 2.2.2: Both the client and the server MUST know if there is
377*cda5da8dSAndroid Build Coastguard Worker        # a TLS session active.  A client MUST NOT attempt to start a TLS
378*cda5da8dSAndroid Build Coastguard Worker        # session if a TLS session is already active.
379*cda5da8dSAndroid Build Coastguard Worker        self.tls_on = False
380*cda5da8dSAndroid Build Coastguard Worker
381*cda5da8dSAndroid Build Coastguard Worker        # Log in and encryption setup order is left to subclasses.
382*cda5da8dSAndroid Build Coastguard Worker        self.authenticated = False
383*cda5da8dSAndroid Build Coastguard Worker
384*cda5da8dSAndroid Build Coastguard Worker    def __enter__(self):
385*cda5da8dSAndroid Build Coastguard Worker        return self
386*cda5da8dSAndroid Build Coastguard Worker
387*cda5da8dSAndroid Build Coastguard Worker    def __exit__(self, *args):
388*cda5da8dSAndroid Build Coastguard Worker        is_connected = lambda: hasattr(self, "file")
389*cda5da8dSAndroid Build Coastguard Worker        if is_connected():
390*cda5da8dSAndroid Build Coastguard Worker            try:
391*cda5da8dSAndroid Build Coastguard Worker                self.quit()
392*cda5da8dSAndroid Build Coastguard Worker            except (OSError, EOFError):
393*cda5da8dSAndroid Build Coastguard Worker                pass
394*cda5da8dSAndroid Build Coastguard Worker            finally:
395*cda5da8dSAndroid Build Coastguard Worker                if is_connected():
396*cda5da8dSAndroid Build Coastguard Worker                    self._close()
397*cda5da8dSAndroid Build Coastguard Worker
398*cda5da8dSAndroid Build Coastguard Worker    def _create_socket(self, timeout):
399*cda5da8dSAndroid Build Coastguard Worker        if timeout is not None and not timeout:
400*cda5da8dSAndroid Build Coastguard Worker            raise ValueError('Non-blocking socket (timeout=0) is not supported')
401*cda5da8dSAndroid Build Coastguard Worker        sys.audit("nntplib.connect", self, self.host, self.port)
402*cda5da8dSAndroid Build Coastguard Worker        return socket.create_connection((self.host, self.port), timeout)
403*cda5da8dSAndroid Build Coastguard Worker
404*cda5da8dSAndroid Build Coastguard Worker    def getwelcome(self):
405*cda5da8dSAndroid Build Coastguard Worker        """Get the welcome message from the server
406*cda5da8dSAndroid Build Coastguard Worker        (this is read and squirreled away by __init__()).
407*cda5da8dSAndroid Build Coastguard Worker        If the response code is 200, posting is allowed;
408*cda5da8dSAndroid Build Coastguard Worker        if it 201, posting is not allowed."""
409*cda5da8dSAndroid Build Coastguard Worker
410*cda5da8dSAndroid Build Coastguard Worker        if self.debugging: print('*welcome*', repr(self.welcome))
411*cda5da8dSAndroid Build Coastguard Worker        return self.welcome
412*cda5da8dSAndroid Build Coastguard Worker
413*cda5da8dSAndroid Build Coastguard Worker    def getcapabilities(self):
414*cda5da8dSAndroid Build Coastguard Worker        """Get the server capabilities, as read by __init__().
415*cda5da8dSAndroid Build Coastguard Worker        If the CAPABILITIES command is not supported, an empty dict is
416*cda5da8dSAndroid Build Coastguard Worker        returned."""
417*cda5da8dSAndroid Build Coastguard Worker        if self._caps is None:
418*cda5da8dSAndroid Build Coastguard Worker            self.nntp_version = 1
419*cda5da8dSAndroid Build Coastguard Worker            self.nntp_implementation = None
420*cda5da8dSAndroid Build Coastguard Worker            try:
421*cda5da8dSAndroid Build Coastguard Worker                resp, caps = self.capabilities()
422*cda5da8dSAndroid Build Coastguard Worker            except (NNTPPermanentError, NNTPTemporaryError):
423*cda5da8dSAndroid Build Coastguard Worker                # Server doesn't support capabilities
424*cda5da8dSAndroid Build Coastguard Worker                self._caps = {}
425*cda5da8dSAndroid Build Coastguard Worker            else:
426*cda5da8dSAndroid Build Coastguard Worker                self._caps = caps
427*cda5da8dSAndroid Build Coastguard Worker                if 'VERSION' in caps:
428*cda5da8dSAndroid Build Coastguard Worker                    # The server can advertise several supported versions,
429*cda5da8dSAndroid Build Coastguard Worker                    # choose the highest.
430*cda5da8dSAndroid Build Coastguard Worker                    self.nntp_version = max(map(int, caps['VERSION']))
431*cda5da8dSAndroid Build Coastguard Worker                if 'IMPLEMENTATION' in caps:
432*cda5da8dSAndroid Build Coastguard Worker                    self.nntp_implementation = ' '.join(caps['IMPLEMENTATION'])
433*cda5da8dSAndroid Build Coastguard Worker        return self._caps
434*cda5da8dSAndroid Build Coastguard Worker
435*cda5da8dSAndroid Build Coastguard Worker    def set_debuglevel(self, level):
436*cda5da8dSAndroid Build Coastguard Worker        """Set the debugging level.  Argument 'level' means:
437*cda5da8dSAndroid Build Coastguard Worker        0: no debugging output (default)
438*cda5da8dSAndroid Build Coastguard Worker        1: print commands and responses but not body text etc.
439*cda5da8dSAndroid Build Coastguard Worker        2: also print raw lines read and sent before stripping CR/LF"""
440*cda5da8dSAndroid Build Coastguard Worker
441*cda5da8dSAndroid Build Coastguard Worker        self.debugging = level
442*cda5da8dSAndroid Build Coastguard Worker    debug = set_debuglevel
443*cda5da8dSAndroid Build Coastguard Worker
444*cda5da8dSAndroid Build Coastguard Worker    def _putline(self, line):
445*cda5da8dSAndroid Build Coastguard Worker        """Internal: send one line to the server, appending CRLF.
446*cda5da8dSAndroid Build Coastguard Worker        The `line` must be a bytes-like object."""
447*cda5da8dSAndroid Build Coastguard Worker        sys.audit("nntplib.putline", self, line)
448*cda5da8dSAndroid Build Coastguard Worker        line = line + _CRLF
449*cda5da8dSAndroid Build Coastguard Worker        if self.debugging > 1: print('*put*', repr(line))
450*cda5da8dSAndroid Build Coastguard Worker        self.file.write(line)
451*cda5da8dSAndroid Build Coastguard Worker        self.file.flush()
452*cda5da8dSAndroid Build Coastguard Worker
453*cda5da8dSAndroid Build Coastguard Worker    def _putcmd(self, line):
454*cda5da8dSAndroid Build Coastguard Worker        """Internal: send one command to the server (through _putline()).
455*cda5da8dSAndroid Build Coastguard Worker        The `line` must be a unicode string."""
456*cda5da8dSAndroid Build Coastguard Worker        if self.debugging: print('*cmd*', repr(line))
457*cda5da8dSAndroid Build Coastguard Worker        line = line.encode(self.encoding, self.errors)
458*cda5da8dSAndroid Build Coastguard Worker        self._putline(line)
459*cda5da8dSAndroid Build Coastguard Worker
460*cda5da8dSAndroid Build Coastguard Worker    def _getline(self, strip_crlf=True):
461*cda5da8dSAndroid Build Coastguard Worker        """Internal: return one line from the server, stripping _CRLF.
462*cda5da8dSAndroid Build Coastguard Worker        Raise EOFError if the connection is closed.
463*cda5da8dSAndroid Build Coastguard Worker        Returns a bytes object."""
464*cda5da8dSAndroid Build Coastguard Worker        line = self.file.readline(_MAXLINE +1)
465*cda5da8dSAndroid Build Coastguard Worker        if len(line) > _MAXLINE:
466*cda5da8dSAndroid Build Coastguard Worker            raise NNTPDataError('line too long')
467*cda5da8dSAndroid Build Coastguard Worker        if self.debugging > 1:
468*cda5da8dSAndroid Build Coastguard Worker            print('*get*', repr(line))
469*cda5da8dSAndroid Build Coastguard Worker        if not line: raise EOFError
470*cda5da8dSAndroid Build Coastguard Worker        if strip_crlf:
471*cda5da8dSAndroid Build Coastguard Worker            if line[-2:] == _CRLF:
472*cda5da8dSAndroid Build Coastguard Worker                line = line[:-2]
473*cda5da8dSAndroid Build Coastguard Worker            elif line[-1:] in _CRLF:
474*cda5da8dSAndroid Build Coastguard Worker                line = line[:-1]
475*cda5da8dSAndroid Build Coastguard Worker        return line
476*cda5da8dSAndroid Build Coastguard Worker
477*cda5da8dSAndroid Build Coastguard Worker    def _getresp(self):
478*cda5da8dSAndroid Build Coastguard Worker        """Internal: get a response from the server.
479*cda5da8dSAndroid Build Coastguard Worker        Raise various errors if the response indicates an error.
480*cda5da8dSAndroid Build Coastguard Worker        Returns a unicode string."""
481*cda5da8dSAndroid Build Coastguard Worker        resp = self._getline()
482*cda5da8dSAndroid Build Coastguard Worker        if self.debugging: print('*resp*', repr(resp))
483*cda5da8dSAndroid Build Coastguard Worker        resp = resp.decode(self.encoding, self.errors)
484*cda5da8dSAndroid Build Coastguard Worker        c = resp[:1]
485*cda5da8dSAndroid Build Coastguard Worker        if c == '4':
486*cda5da8dSAndroid Build Coastguard Worker            raise NNTPTemporaryError(resp)
487*cda5da8dSAndroid Build Coastguard Worker        if c == '5':
488*cda5da8dSAndroid Build Coastguard Worker            raise NNTPPermanentError(resp)
489*cda5da8dSAndroid Build Coastguard Worker        if c not in '123':
490*cda5da8dSAndroid Build Coastguard Worker            raise NNTPProtocolError(resp)
491*cda5da8dSAndroid Build Coastguard Worker        return resp
492*cda5da8dSAndroid Build Coastguard Worker
493*cda5da8dSAndroid Build Coastguard Worker    def _getlongresp(self, file=None):
494*cda5da8dSAndroid Build Coastguard Worker        """Internal: get a response plus following text from the server.
495*cda5da8dSAndroid Build Coastguard Worker        Raise various errors if the response indicates an error.
496*cda5da8dSAndroid Build Coastguard Worker
497*cda5da8dSAndroid Build Coastguard Worker        Returns a (response, lines) tuple where `response` is a unicode
498*cda5da8dSAndroid Build Coastguard Worker        string and `lines` is a list of bytes objects.
499*cda5da8dSAndroid Build Coastguard Worker        If `file` is a file-like object, it must be open in binary mode.
500*cda5da8dSAndroid Build Coastguard Worker        """
501*cda5da8dSAndroid Build Coastguard Worker
502*cda5da8dSAndroid Build Coastguard Worker        openedFile = None
503*cda5da8dSAndroid Build Coastguard Worker        try:
504*cda5da8dSAndroid Build Coastguard Worker            # If a string was passed then open a file with that name
505*cda5da8dSAndroid Build Coastguard Worker            if isinstance(file, (str, bytes)):
506*cda5da8dSAndroid Build Coastguard Worker                openedFile = file = open(file, "wb")
507*cda5da8dSAndroid Build Coastguard Worker
508*cda5da8dSAndroid Build Coastguard Worker            resp = self._getresp()
509*cda5da8dSAndroid Build Coastguard Worker            if resp[:3] not in _LONGRESP:
510*cda5da8dSAndroid Build Coastguard Worker                raise NNTPReplyError(resp)
511*cda5da8dSAndroid Build Coastguard Worker
512*cda5da8dSAndroid Build Coastguard Worker            lines = []
513*cda5da8dSAndroid Build Coastguard Worker            if file is not None:
514*cda5da8dSAndroid Build Coastguard Worker                # XXX lines = None instead?
515*cda5da8dSAndroid Build Coastguard Worker                terminators = (b'.' + _CRLF, b'.\n')
516*cda5da8dSAndroid Build Coastguard Worker                while 1:
517*cda5da8dSAndroid Build Coastguard Worker                    line = self._getline(False)
518*cda5da8dSAndroid Build Coastguard Worker                    if line in terminators:
519*cda5da8dSAndroid Build Coastguard Worker                        break
520*cda5da8dSAndroid Build Coastguard Worker                    if line.startswith(b'..'):
521*cda5da8dSAndroid Build Coastguard Worker                        line = line[1:]
522*cda5da8dSAndroid Build Coastguard Worker                    file.write(line)
523*cda5da8dSAndroid Build Coastguard Worker            else:
524*cda5da8dSAndroid Build Coastguard Worker                terminator = b'.'
525*cda5da8dSAndroid Build Coastguard Worker                while 1:
526*cda5da8dSAndroid Build Coastguard Worker                    line = self._getline()
527*cda5da8dSAndroid Build Coastguard Worker                    if line == terminator:
528*cda5da8dSAndroid Build Coastguard Worker                        break
529*cda5da8dSAndroid Build Coastguard Worker                    if line.startswith(b'..'):
530*cda5da8dSAndroid Build Coastguard Worker                        line = line[1:]
531*cda5da8dSAndroid Build Coastguard Worker                    lines.append(line)
532*cda5da8dSAndroid Build Coastguard Worker        finally:
533*cda5da8dSAndroid Build Coastguard Worker            # If this method created the file, then it must close it
534*cda5da8dSAndroid Build Coastguard Worker            if openedFile:
535*cda5da8dSAndroid Build Coastguard Worker                openedFile.close()
536*cda5da8dSAndroid Build Coastguard Worker
537*cda5da8dSAndroid Build Coastguard Worker        return resp, lines
538*cda5da8dSAndroid Build Coastguard Worker
539*cda5da8dSAndroid Build Coastguard Worker    def _shortcmd(self, line):
540*cda5da8dSAndroid Build Coastguard Worker        """Internal: send a command and get the response.
541*cda5da8dSAndroid Build Coastguard Worker        Same return value as _getresp()."""
542*cda5da8dSAndroid Build Coastguard Worker        self._putcmd(line)
543*cda5da8dSAndroid Build Coastguard Worker        return self._getresp()
544*cda5da8dSAndroid Build Coastguard Worker
545*cda5da8dSAndroid Build Coastguard Worker    def _longcmd(self, line, file=None):
546*cda5da8dSAndroid Build Coastguard Worker        """Internal: send a command and get the response plus following text.
547*cda5da8dSAndroid Build Coastguard Worker        Same return value as _getlongresp()."""
548*cda5da8dSAndroid Build Coastguard Worker        self._putcmd(line)
549*cda5da8dSAndroid Build Coastguard Worker        return self._getlongresp(file)
550*cda5da8dSAndroid Build Coastguard Worker
551*cda5da8dSAndroid Build Coastguard Worker    def _longcmdstring(self, line, file=None):
552*cda5da8dSAndroid Build Coastguard Worker        """Internal: send a command and get the response plus following text.
553*cda5da8dSAndroid Build Coastguard Worker        Same as _longcmd() and _getlongresp(), except that the returned `lines`
554*cda5da8dSAndroid Build Coastguard Worker        are unicode strings rather than bytes objects.
555*cda5da8dSAndroid Build Coastguard Worker        """
556*cda5da8dSAndroid Build Coastguard Worker        self._putcmd(line)
557*cda5da8dSAndroid Build Coastguard Worker        resp, list = self._getlongresp(file)
558*cda5da8dSAndroid Build Coastguard Worker        return resp, [line.decode(self.encoding, self.errors)
559*cda5da8dSAndroid Build Coastguard Worker                      for line in list]
560*cda5da8dSAndroid Build Coastguard Worker
561*cda5da8dSAndroid Build Coastguard Worker    def _getoverviewfmt(self):
562*cda5da8dSAndroid Build Coastguard Worker        """Internal: get the overview format. Queries the server if not
563*cda5da8dSAndroid Build Coastguard Worker        already done, else returns the cached value."""
564*cda5da8dSAndroid Build Coastguard Worker        try:
565*cda5da8dSAndroid Build Coastguard Worker            return self._cachedoverviewfmt
566*cda5da8dSAndroid Build Coastguard Worker        except AttributeError:
567*cda5da8dSAndroid Build Coastguard Worker            pass
568*cda5da8dSAndroid Build Coastguard Worker        try:
569*cda5da8dSAndroid Build Coastguard Worker            resp, lines = self._longcmdstring("LIST OVERVIEW.FMT")
570*cda5da8dSAndroid Build Coastguard Worker        except NNTPPermanentError:
571*cda5da8dSAndroid Build Coastguard Worker            # Not supported by server?
572*cda5da8dSAndroid Build Coastguard Worker            fmt = _DEFAULT_OVERVIEW_FMT[:]
573*cda5da8dSAndroid Build Coastguard Worker        else:
574*cda5da8dSAndroid Build Coastguard Worker            fmt = _parse_overview_fmt(lines)
575*cda5da8dSAndroid Build Coastguard Worker        self._cachedoverviewfmt = fmt
576*cda5da8dSAndroid Build Coastguard Worker        return fmt
577*cda5da8dSAndroid Build Coastguard Worker
578*cda5da8dSAndroid Build Coastguard Worker    def _grouplist(self, lines):
579*cda5da8dSAndroid Build Coastguard Worker        # Parse lines into "group last first flag"
580*cda5da8dSAndroid Build Coastguard Worker        return [GroupInfo(*line.split()) for line in lines]
581*cda5da8dSAndroid Build Coastguard Worker
582*cda5da8dSAndroid Build Coastguard Worker    def capabilities(self):
583*cda5da8dSAndroid Build Coastguard Worker        """Process a CAPABILITIES command.  Not supported by all servers.
584*cda5da8dSAndroid Build Coastguard Worker        Return:
585*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
586*cda5da8dSAndroid Build Coastguard Worker        - caps: a dictionary mapping capability names to lists of tokens
587*cda5da8dSAndroid Build Coastguard Worker        (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] })
588*cda5da8dSAndroid Build Coastguard Worker        """
589*cda5da8dSAndroid Build Coastguard Worker        caps = {}
590*cda5da8dSAndroid Build Coastguard Worker        resp, lines = self._longcmdstring("CAPABILITIES")
591*cda5da8dSAndroid Build Coastguard Worker        for line in lines:
592*cda5da8dSAndroid Build Coastguard Worker            name, *tokens = line.split()
593*cda5da8dSAndroid Build Coastguard Worker            caps[name] = tokens
594*cda5da8dSAndroid Build Coastguard Worker        return resp, caps
595*cda5da8dSAndroid Build Coastguard Worker
596*cda5da8dSAndroid Build Coastguard Worker    def newgroups(self, date, *, file=None):
597*cda5da8dSAndroid Build Coastguard Worker        """Process a NEWGROUPS command.  Arguments:
598*cda5da8dSAndroid Build Coastguard Worker        - date: a date or datetime object
599*cda5da8dSAndroid Build Coastguard Worker        Return:
600*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
601*cda5da8dSAndroid Build Coastguard Worker        - list: list of newsgroup names
602*cda5da8dSAndroid Build Coastguard Worker        """
603*cda5da8dSAndroid Build Coastguard Worker        if not isinstance(date, (datetime.date, datetime.date)):
604*cda5da8dSAndroid Build Coastguard Worker            raise TypeError(
605*cda5da8dSAndroid Build Coastguard Worker                "the date parameter must be a date or datetime object, "
606*cda5da8dSAndroid Build Coastguard Worker                "not '{:40}'".format(date.__class__.__name__))
607*cda5da8dSAndroid Build Coastguard Worker        date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
608*cda5da8dSAndroid Build Coastguard Worker        cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str)
609*cda5da8dSAndroid Build Coastguard Worker        resp, lines = self._longcmdstring(cmd, file)
610*cda5da8dSAndroid Build Coastguard Worker        return resp, self._grouplist(lines)
611*cda5da8dSAndroid Build Coastguard Worker
612*cda5da8dSAndroid Build Coastguard Worker    def newnews(self, group, date, *, file=None):
613*cda5da8dSAndroid Build Coastguard Worker        """Process a NEWNEWS command.  Arguments:
614*cda5da8dSAndroid Build Coastguard Worker        - group: group name or '*'
615*cda5da8dSAndroid Build Coastguard Worker        - date: a date or datetime object
616*cda5da8dSAndroid Build Coastguard Worker        Return:
617*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
618*cda5da8dSAndroid Build Coastguard Worker        - list: list of message ids
619*cda5da8dSAndroid Build Coastguard Worker        """
620*cda5da8dSAndroid Build Coastguard Worker        if not isinstance(date, (datetime.date, datetime.date)):
621*cda5da8dSAndroid Build Coastguard Worker            raise TypeError(
622*cda5da8dSAndroid Build Coastguard Worker                "the date parameter must be a date or datetime object, "
623*cda5da8dSAndroid Build Coastguard Worker                "not '{:40}'".format(date.__class__.__name__))
624*cda5da8dSAndroid Build Coastguard Worker        date_str, time_str = _unparse_datetime(date, self.nntp_version < 2)
625*cda5da8dSAndroid Build Coastguard Worker        cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str)
626*cda5da8dSAndroid Build Coastguard Worker        return self._longcmdstring(cmd, file)
627*cda5da8dSAndroid Build Coastguard Worker
628*cda5da8dSAndroid Build Coastguard Worker    def list(self, group_pattern=None, *, file=None):
629*cda5da8dSAndroid Build Coastguard Worker        """Process a LIST or LIST ACTIVE command. Arguments:
630*cda5da8dSAndroid Build Coastguard Worker        - group_pattern: a pattern indicating which groups to query
631*cda5da8dSAndroid Build Coastguard Worker        - file: Filename string or file object to store the result in
632*cda5da8dSAndroid Build Coastguard Worker        Returns:
633*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
634*cda5da8dSAndroid Build Coastguard Worker        - list: list of (group, last, first, flag) (strings)
635*cda5da8dSAndroid Build Coastguard Worker        """
636*cda5da8dSAndroid Build Coastguard Worker        if group_pattern is not None:
637*cda5da8dSAndroid Build Coastguard Worker            command = 'LIST ACTIVE ' + group_pattern
638*cda5da8dSAndroid Build Coastguard Worker        else:
639*cda5da8dSAndroid Build Coastguard Worker            command = 'LIST'
640*cda5da8dSAndroid Build Coastguard Worker        resp, lines = self._longcmdstring(command, file)
641*cda5da8dSAndroid Build Coastguard Worker        return resp, self._grouplist(lines)
642*cda5da8dSAndroid Build Coastguard Worker
643*cda5da8dSAndroid Build Coastguard Worker    def _getdescriptions(self, group_pattern, return_all):
644*cda5da8dSAndroid Build Coastguard Worker        line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$')
645*cda5da8dSAndroid Build Coastguard Worker        # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first
646*cda5da8dSAndroid Build Coastguard Worker        resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern)
647*cda5da8dSAndroid Build Coastguard Worker        if not resp.startswith('215'):
648*cda5da8dSAndroid Build Coastguard Worker            # Now the deprecated XGTITLE.  This either raises an error
649*cda5da8dSAndroid Build Coastguard Worker            # or succeeds with the same output structure as LIST
650*cda5da8dSAndroid Build Coastguard Worker            # NEWSGROUPS.
651*cda5da8dSAndroid Build Coastguard Worker            resp, lines = self._longcmdstring('XGTITLE ' + group_pattern)
652*cda5da8dSAndroid Build Coastguard Worker        groups = {}
653*cda5da8dSAndroid Build Coastguard Worker        for raw_line in lines:
654*cda5da8dSAndroid Build Coastguard Worker            match = line_pat.search(raw_line.strip())
655*cda5da8dSAndroid Build Coastguard Worker            if match:
656*cda5da8dSAndroid Build Coastguard Worker                name, desc = match.group(1, 2)
657*cda5da8dSAndroid Build Coastguard Worker                if not return_all:
658*cda5da8dSAndroid Build Coastguard Worker                    return desc
659*cda5da8dSAndroid Build Coastguard Worker                groups[name] = desc
660*cda5da8dSAndroid Build Coastguard Worker        if return_all:
661*cda5da8dSAndroid Build Coastguard Worker            return resp, groups
662*cda5da8dSAndroid Build Coastguard Worker        else:
663*cda5da8dSAndroid Build Coastguard Worker            # Nothing found
664*cda5da8dSAndroid Build Coastguard Worker            return ''
665*cda5da8dSAndroid Build Coastguard Worker
666*cda5da8dSAndroid Build Coastguard Worker    def description(self, group):
667*cda5da8dSAndroid Build Coastguard Worker        """Get a description for a single group.  If more than one
668*cda5da8dSAndroid Build Coastguard Worker        group matches ('group' is a pattern), return the first.  If no
669*cda5da8dSAndroid Build Coastguard Worker        group matches, return an empty string.
670*cda5da8dSAndroid Build Coastguard Worker
671*cda5da8dSAndroid Build Coastguard Worker        This elides the response code from the server, since it can
672*cda5da8dSAndroid Build Coastguard Worker        only be '215' or '285' (for xgtitle) anyway.  If the response
673*cda5da8dSAndroid Build Coastguard Worker        code is needed, use the 'descriptions' method.
674*cda5da8dSAndroid Build Coastguard Worker
675*cda5da8dSAndroid Build Coastguard Worker        NOTE: This neither checks for a wildcard in 'group' nor does
676*cda5da8dSAndroid Build Coastguard Worker        it check whether the group actually exists."""
677*cda5da8dSAndroid Build Coastguard Worker        return self._getdescriptions(group, False)
678*cda5da8dSAndroid Build Coastguard Worker
679*cda5da8dSAndroid Build Coastguard Worker    def descriptions(self, group_pattern):
680*cda5da8dSAndroid Build Coastguard Worker        """Get descriptions for a range of groups."""
681*cda5da8dSAndroid Build Coastguard Worker        return self._getdescriptions(group_pattern, True)
682*cda5da8dSAndroid Build Coastguard Worker
683*cda5da8dSAndroid Build Coastguard Worker    def group(self, name):
684*cda5da8dSAndroid Build Coastguard Worker        """Process a GROUP command.  Argument:
685*cda5da8dSAndroid Build Coastguard Worker        - group: the group name
686*cda5da8dSAndroid Build Coastguard Worker        Returns:
687*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
688*cda5da8dSAndroid Build Coastguard Worker        - count: number of articles
689*cda5da8dSAndroid Build Coastguard Worker        - first: first article number
690*cda5da8dSAndroid Build Coastguard Worker        - last: last article number
691*cda5da8dSAndroid Build Coastguard Worker        - name: the group name
692*cda5da8dSAndroid Build Coastguard Worker        """
693*cda5da8dSAndroid Build Coastguard Worker        resp = self._shortcmd('GROUP ' + name)
694*cda5da8dSAndroid Build Coastguard Worker        if not resp.startswith('211'):
695*cda5da8dSAndroid Build Coastguard Worker            raise NNTPReplyError(resp)
696*cda5da8dSAndroid Build Coastguard Worker        words = resp.split()
697*cda5da8dSAndroid Build Coastguard Worker        count = first = last = 0
698*cda5da8dSAndroid Build Coastguard Worker        n = len(words)
699*cda5da8dSAndroid Build Coastguard Worker        if n > 1:
700*cda5da8dSAndroid Build Coastguard Worker            count = words[1]
701*cda5da8dSAndroid Build Coastguard Worker            if n > 2:
702*cda5da8dSAndroid Build Coastguard Worker                first = words[2]
703*cda5da8dSAndroid Build Coastguard Worker                if n > 3:
704*cda5da8dSAndroid Build Coastguard Worker                    last = words[3]
705*cda5da8dSAndroid Build Coastguard Worker                    if n > 4:
706*cda5da8dSAndroid Build Coastguard Worker                        name = words[4].lower()
707*cda5da8dSAndroid Build Coastguard Worker        return resp, int(count), int(first), int(last), name
708*cda5da8dSAndroid Build Coastguard Worker
709*cda5da8dSAndroid Build Coastguard Worker    def help(self, *, file=None):
710*cda5da8dSAndroid Build Coastguard Worker        """Process a HELP command. Argument:
711*cda5da8dSAndroid Build Coastguard Worker        - file: Filename string or file object to store the result in
712*cda5da8dSAndroid Build Coastguard Worker        Returns:
713*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
714*cda5da8dSAndroid Build Coastguard Worker        - list: list of strings returned by the server in response to the
715*cda5da8dSAndroid Build Coastguard Worker                HELP command
716*cda5da8dSAndroid Build Coastguard Worker        """
717*cda5da8dSAndroid Build Coastguard Worker        return self._longcmdstring('HELP', file)
718*cda5da8dSAndroid Build Coastguard Worker
719*cda5da8dSAndroid Build Coastguard Worker    def _statparse(self, resp):
720*cda5da8dSAndroid Build Coastguard Worker        """Internal: parse the response line of a STAT, NEXT, LAST,
721*cda5da8dSAndroid Build Coastguard Worker        ARTICLE, HEAD or BODY command."""
722*cda5da8dSAndroid Build Coastguard Worker        if not resp.startswith('22'):
723*cda5da8dSAndroid Build Coastguard Worker            raise NNTPReplyError(resp)
724*cda5da8dSAndroid Build Coastguard Worker        words = resp.split()
725*cda5da8dSAndroid Build Coastguard Worker        art_num = int(words[1])
726*cda5da8dSAndroid Build Coastguard Worker        message_id = words[2]
727*cda5da8dSAndroid Build Coastguard Worker        return resp, art_num, message_id
728*cda5da8dSAndroid Build Coastguard Worker
729*cda5da8dSAndroid Build Coastguard Worker    def _statcmd(self, line):
730*cda5da8dSAndroid Build Coastguard Worker        """Internal: process a STAT, NEXT or LAST command."""
731*cda5da8dSAndroid Build Coastguard Worker        resp = self._shortcmd(line)
732*cda5da8dSAndroid Build Coastguard Worker        return self._statparse(resp)
733*cda5da8dSAndroid Build Coastguard Worker
734*cda5da8dSAndroid Build Coastguard Worker    def stat(self, message_spec=None):
735*cda5da8dSAndroid Build Coastguard Worker        """Process a STAT command.  Argument:
736*cda5da8dSAndroid Build Coastguard Worker        - message_spec: article number or message id (if not specified,
737*cda5da8dSAndroid Build Coastguard Worker          the current article is selected)
738*cda5da8dSAndroid Build Coastguard Worker        Returns:
739*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
740*cda5da8dSAndroid Build Coastguard Worker        - art_num: the article number
741*cda5da8dSAndroid Build Coastguard Worker        - message_id: the message id
742*cda5da8dSAndroid Build Coastguard Worker        """
743*cda5da8dSAndroid Build Coastguard Worker        if message_spec:
744*cda5da8dSAndroid Build Coastguard Worker            return self._statcmd('STAT {0}'.format(message_spec))
745*cda5da8dSAndroid Build Coastguard Worker        else:
746*cda5da8dSAndroid Build Coastguard Worker            return self._statcmd('STAT')
747*cda5da8dSAndroid Build Coastguard Worker
748*cda5da8dSAndroid Build Coastguard Worker    def next(self):
749*cda5da8dSAndroid Build Coastguard Worker        """Process a NEXT command.  No arguments.  Return as for STAT."""
750*cda5da8dSAndroid Build Coastguard Worker        return self._statcmd('NEXT')
751*cda5da8dSAndroid Build Coastguard Worker
752*cda5da8dSAndroid Build Coastguard Worker    def last(self):
753*cda5da8dSAndroid Build Coastguard Worker        """Process a LAST command.  No arguments.  Return as for STAT."""
754*cda5da8dSAndroid Build Coastguard Worker        return self._statcmd('LAST')
755*cda5da8dSAndroid Build Coastguard Worker
756*cda5da8dSAndroid Build Coastguard Worker    def _artcmd(self, line, file=None):
757*cda5da8dSAndroid Build Coastguard Worker        """Internal: process a HEAD, BODY or ARTICLE command."""
758*cda5da8dSAndroid Build Coastguard Worker        resp, lines = self._longcmd(line, file)
759*cda5da8dSAndroid Build Coastguard Worker        resp, art_num, message_id = self._statparse(resp)
760*cda5da8dSAndroid Build Coastguard Worker        return resp, ArticleInfo(art_num, message_id, lines)
761*cda5da8dSAndroid Build Coastguard Worker
762*cda5da8dSAndroid Build Coastguard Worker    def head(self, message_spec=None, *, file=None):
763*cda5da8dSAndroid Build Coastguard Worker        """Process a HEAD command.  Argument:
764*cda5da8dSAndroid Build Coastguard Worker        - message_spec: article number or message id
765*cda5da8dSAndroid Build Coastguard Worker        - file: filename string or file object to store the headers in
766*cda5da8dSAndroid Build Coastguard Worker        Returns:
767*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
768*cda5da8dSAndroid Build Coastguard Worker        - ArticleInfo: (article number, message id, list of header lines)
769*cda5da8dSAndroid Build Coastguard Worker        """
770*cda5da8dSAndroid Build Coastguard Worker        if message_spec is not None:
771*cda5da8dSAndroid Build Coastguard Worker            cmd = 'HEAD {0}'.format(message_spec)
772*cda5da8dSAndroid Build Coastguard Worker        else:
773*cda5da8dSAndroid Build Coastguard Worker            cmd = 'HEAD'
774*cda5da8dSAndroid Build Coastguard Worker        return self._artcmd(cmd, file)
775*cda5da8dSAndroid Build Coastguard Worker
776*cda5da8dSAndroid Build Coastguard Worker    def body(self, message_spec=None, *, file=None):
777*cda5da8dSAndroid Build Coastguard Worker        """Process a BODY command.  Argument:
778*cda5da8dSAndroid Build Coastguard Worker        - message_spec: article number or message id
779*cda5da8dSAndroid Build Coastguard Worker        - file: filename string or file object to store the body in
780*cda5da8dSAndroid Build Coastguard Worker        Returns:
781*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
782*cda5da8dSAndroid Build Coastguard Worker        - ArticleInfo: (article number, message id, list of body lines)
783*cda5da8dSAndroid Build Coastguard Worker        """
784*cda5da8dSAndroid Build Coastguard Worker        if message_spec is not None:
785*cda5da8dSAndroid Build Coastguard Worker            cmd = 'BODY {0}'.format(message_spec)
786*cda5da8dSAndroid Build Coastguard Worker        else:
787*cda5da8dSAndroid Build Coastguard Worker            cmd = 'BODY'
788*cda5da8dSAndroid Build Coastguard Worker        return self._artcmd(cmd, file)
789*cda5da8dSAndroid Build Coastguard Worker
790*cda5da8dSAndroid Build Coastguard Worker    def article(self, message_spec=None, *, file=None):
791*cda5da8dSAndroid Build Coastguard Worker        """Process an ARTICLE command.  Argument:
792*cda5da8dSAndroid Build Coastguard Worker        - message_spec: article number or message id
793*cda5da8dSAndroid Build Coastguard Worker        - file: filename string or file object to store the article in
794*cda5da8dSAndroid Build Coastguard Worker        Returns:
795*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
796*cda5da8dSAndroid Build Coastguard Worker        - ArticleInfo: (article number, message id, list of article lines)
797*cda5da8dSAndroid Build Coastguard Worker        """
798*cda5da8dSAndroid Build Coastguard Worker        if message_spec is not None:
799*cda5da8dSAndroid Build Coastguard Worker            cmd = 'ARTICLE {0}'.format(message_spec)
800*cda5da8dSAndroid Build Coastguard Worker        else:
801*cda5da8dSAndroid Build Coastguard Worker            cmd = 'ARTICLE'
802*cda5da8dSAndroid Build Coastguard Worker        return self._artcmd(cmd, file)
803*cda5da8dSAndroid Build Coastguard Worker
804*cda5da8dSAndroid Build Coastguard Worker    def slave(self):
805*cda5da8dSAndroid Build Coastguard Worker        """Process a SLAVE command.  Returns:
806*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
807*cda5da8dSAndroid Build Coastguard Worker        """
808*cda5da8dSAndroid Build Coastguard Worker        return self._shortcmd('SLAVE')
809*cda5da8dSAndroid Build Coastguard Worker
810*cda5da8dSAndroid Build Coastguard Worker    def xhdr(self, hdr, str, *, file=None):
811*cda5da8dSAndroid Build Coastguard Worker        """Process an XHDR command (optional server extension).  Arguments:
812*cda5da8dSAndroid Build Coastguard Worker        - hdr: the header type (e.g. 'subject')
813*cda5da8dSAndroid Build Coastguard Worker        - str: an article nr, a message id, or a range nr1-nr2
814*cda5da8dSAndroid Build Coastguard Worker        - file: Filename string or file object to store the result in
815*cda5da8dSAndroid Build Coastguard Worker        Returns:
816*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
817*cda5da8dSAndroid Build Coastguard Worker        - list: list of (nr, value) strings
818*cda5da8dSAndroid Build Coastguard Worker        """
819*cda5da8dSAndroid Build Coastguard Worker        pat = re.compile('^([0-9]+) ?(.*)\n?')
820*cda5da8dSAndroid Build Coastguard Worker        resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file)
821*cda5da8dSAndroid Build Coastguard Worker        def remove_number(line):
822*cda5da8dSAndroid Build Coastguard Worker            m = pat.match(line)
823*cda5da8dSAndroid Build Coastguard Worker            return m.group(1, 2) if m else line
824*cda5da8dSAndroid Build Coastguard Worker        return resp, [remove_number(line) for line in lines]
825*cda5da8dSAndroid Build Coastguard Worker
826*cda5da8dSAndroid Build Coastguard Worker    def xover(self, start, end, *, file=None):
827*cda5da8dSAndroid Build Coastguard Worker        """Process an XOVER command (optional server extension) Arguments:
828*cda5da8dSAndroid Build Coastguard Worker        - start: start of range
829*cda5da8dSAndroid Build Coastguard Worker        - end: end of range
830*cda5da8dSAndroid Build Coastguard Worker        - file: Filename string or file object to store the result in
831*cda5da8dSAndroid Build Coastguard Worker        Returns:
832*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
833*cda5da8dSAndroid Build Coastguard Worker        - list: list of dicts containing the response fields
834*cda5da8dSAndroid Build Coastguard Worker        """
835*cda5da8dSAndroid Build Coastguard Worker        resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end),
836*cda5da8dSAndroid Build Coastguard Worker                                          file)
837*cda5da8dSAndroid Build Coastguard Worker        fmt = self._getoverviewfmt()
838*cda5da8dSAndroid Build Coastguard Worker        return resp, _parse_overview(lines, fmt)
839*cda5da8dSAndroid Build Coastguard Worker
840*cda5da8dSAndroid Build Coastguard Worker    def over(self, message_spec, *, file=None):
841*cda5da8dSAndroid Build Coastguard Worker        """Process an OVER command.  If the command isn't supported, fall
842*cda5da8dSAndroid Build Coastguard Worker        back to XOVER. Arguments:
843*cda5da8dSAndroid Build Coastguard Worker        - message_spec:
844*cda5da8dSAndroid Build Coastguard Worker            - either a message id, indicating the article to fetch
845*cda5da8dSAndroid Build Coastguard Worker              information about
846*cda5da8dSAndroid Build Coastguard Worker            - or a (start, end) tuple, indicating a range of article numbers;
847*cda5da8dSAndroid Build Coastguard Worker              if end is None, information up to the newest message will be
848*cda5da8dSAndroid Build Coastguard Worker              retrieved
849*cda5da8dSAndroid Build Coastguard Worker            - or None, indicating the current article number must be used
850*cda5da8dSAndroid Build Coastguard Worker        - file: Filename string or file object to store the result in
851*cda5da8dSAndroid Build Coastguard Worker        Returns:
852*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
853*cda5da8dSAndroid Build Coastguard Worker        - list: list of dicts containing the response fields
854*cda5da8dSAndroid Build Coastguard Worker
855*cda5da8dSAndroid Build Coastguard Worker        NOTE: the "message id" form isn't supported by XOVER
856*cda5da8dSAndroid Build Coastguard Worker        """
857*cda5da8dSAndroid Build Coastguard Worker        cmd = 'OVER' if 'OVER' in self._caps else 'XOVER'
858*cda5da8dSAndroid Build Coastguard Worker        if isinstance(message_spec, (tuple, list)):
859*cda5da8dSAndroid Build Coastguard Worker            start, end = message_spec
860*cda5da8dSAndroid Build Coastguard Worker            cmd += ' {0}-{1}'.format(start, end or '')
861*cda5da8dSAndroid Build Coastguard Worker        elif message_spec is not None:
862*cda5da8dSAndroid Build Coastguard Worker            cmd = cmd + ' ' + message_spec
863*cda5da8dSAndroid Build Coastguard Worker        resp, lines = self._longcmdstring(cmd, file)
864*cda5da8dSAndroid Build Coastguard Worker        fmt = self._getoverviewfmt()
865*cda5da8dSAndroid Build Coastguard Worker        return resp, _parse_overview(lines, fmt)
866*cda5da8dSAndroid Build Coastguard Worker
867*cda5da8dSAndroid Build Coastguard Worker    def date(self):
868*cda5da8dSAndroid Build Coastguard Worker        """Process the DATE command.
869*cda5da8dSAndroid Build Coastguard Worker        Returns:
870*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
871*cda5da8dSAndroid Build Coastguard Worker        - date: datetime object
872*cda5da8dSAndroid Build Coastguard Worker        """
873*cda5da8dSAndroid Build Coastguard Worker        resp = self._shortcmd("DATE")
874*cda5da8dSAndroid Build Coastguard Worker        if not resp.startswith('111'):
875*cda5da8dSAndroid Build Coastguard Worker            raise NNTPReplyError(resp)
876*cda5da8dSAndroid Build Coastguard Worker        elem = resp.split()
877*cda5da8dSAndroid Build Coastguard Worker        if len(elem) != 2:
878*cda5da8dSAndroid Build Coastguard Worker            raise NNTPDataError(resp)
879*cda5da8dSAndroid Build Coastguard Worker        date = elem[1]
880*cda5da8dSAndroid Build Coastguard Worker        if len(date) != 14:
881*cda5da8dSAndroid Build Coastguard Worker            raise NNTPDataError(resp)
882*cda5da8dSAndroid Build Coastguard Worker        return resp, _parse_datetime(date, None)
883*cda5da8dSAndroid Build Coastguard Worker
884*cda5da8dSAndroid Build Coastguard Worker    def _post(self, command, f):
885*cda5da8dSAndroid Build Coastguard Worker        resp = self._shortcmd(command)
886*cda5da8dSAndroid Build Coastguard Worker        # Raises a specific exception if posting is not allowed
887*cda5da8dSAndroid Build Coastguard Worker        if not resp.startswith('3'):
888*cda5da8dSAndroid Build Coastguard Worker            raise NNTPReplyError(resp)
889*cda5da8dSAndroid Build Coastguard Worker        if isinstance(f, (bytes, bytearray)):
890*cda5da8dSAndroid Build Coastguard Worker            f = f.splitlines()
891*cda5da8dSAndroid Build Coastguard Worker        # We don't use _putline() because:
892*cda5da8dSAndroid Build Coastguard Worker        # - we don't want additional CRLF if the file or iterable is already
893*cda5da8dSAndroid Build Coastguard Worker        #   in the right format
894*cda5da8dSAndroid Build Coastguard Worker        # - we don't want a spurious flush() after each line is written
895*cda5da8dSAndroid Build Coastguard Worker        for line in f:
896*cda5da8dSAndroid Build Coastguard Worker            if not line.endswith(_CRLF):
897*cda5da8dSAndroid Build Coastguard Worker                line = line.rstrip(b"\r\n") + _CRLF
898*cda5da8dSAndroid Build Coastguard Worker            if line.startswith(b'.'):
899*cda5da8dSAndroid Build Coastguard Worker                line = b'.' + line
900*cda5da8dSAndroid Build Coastguard Worker            self.file.write(line)
901*cda5da8dSAndroid Build Coastguard Worker        self.file.write(b".\r\n")
902*cda5da8dSAndroid Build Coastguard Worker        self.file.flush()
903*cda5da8dSAndroid Build Coastguard Worker        return self._getresp()
904*cda5da8dSAndroid Build Coastguard Worker
905*cda5da8dSAndroid Build Coastguard Worker    def post(self, data):
906*cda5da8dSAndroid Build Coastguard Worker        """Process a POST command.  Arguments:
907*cda5da8dSAndroid Build Coastguard Worker        - data: bytes object, iterable or file containing the article
908*cda5da8dSAndroid Build Coastguard Worker        Returns:
909*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful"""
910*cda5da8dSAndroid Build Coastguard Worker        return self._post('POST', data)
911*cda5da8dSAndroid Build Coastguard Worker
912*cda5da8dSAndroid Build Coastguard Worker    def ihave(self, message_id, data):
913*cda5da8dSAndroid Build Coastguard Worker        """Process an IHAVE command.  Arguments:
914*cda5da8dSAndroid Build Coastguard Worker        - message_id: message-id of the article
915*cda5da8dSAndroid Build Coastguard Worker        - data: file containing the article
916*cda5da8dSAndroid Build Coastguard Worker        Returns:
917*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful
918*cda5da8dSAndroid Build Coastguard Worker        Note that if the server refuses the article an exception is raised."""
919*cda5da8dSAndroid Build Coastguard Worker        return self._post('IHAVE {0}'.format(message_id), data)
920*cda5da8dSAndroid Build Coastguard Worker
921*cda5da8dSAndroid Build Coastguard Worker    def _close(self):
922*cda5da8dSAndroid Build Coastguard Worker        try:
923*cda5da8dSAndroid Build Coastguard Worker            if self.file:
924*cda5da8dSAndroid Build Coastguard Worker                self.file.close()
925*cda5da8dSAndroid Build Coastguard Worker                del self.file
926*cda5da8dSAndroid Build Coastguard Worker        finally:
927*cda5da8dSAndroid Build Coastguard Worker            self.sock.close()
928*cda5da8dSAndroid Build Coastguard Worker
929*cda5da8dSAndroid Build Coastguard Worker    def quit(self):
930*cda5da8dSAndroid Build Coastguard Worker        """Process a QUIT command and close the socket.  Returns:
931*cda5da8dSAndroid Build Coastguard Worker        - resp: server response if successful"""
932*cda5da8dSAndroid Build Coastguard Worker        try:
933*cda5da8dSAndroid Build Coastguard Worker            resp = self._shortcmd('QUIT')
934*cda5da8dSAndroid Build Coastguard Worker        finally:
935*cda5da8dSAndroid Build Coastguard Worker            self._close()
936*cda5da8dSAndroid Build Coastguard Worker        return resp
937*cda5da8dSAndroid Build Coastguard Worker
938*cda5da8dSAndroid Build Coastguard Worker    def login(self, user=None, password=None, usenetrc=True):
939*cda5da8dSAndroid Build Coastguard Worker        if self.authenticated:
940*cda5da8dSAndroid Build Coastguard Worker            raise ValueError("Already logged in.")
941*cda5da8dSAndroid Build Coastguard Worker        if not user and not usenetrc:
942*cda5da8dSAndroid Build Coastguard Worker            raise ValueError(
943*cda5da8dSAndroid Build Coastguard Worker                "At least one of `user` and `usenetrc` must be specified")
944*cda5da8dSAndroid Build Coastguard Worker        # If no login/password was specified but netrc was requested,
945*cda5da8dSAndroid Build Coastguard Worker        # try to get them from ~/.netrc
946*cda5da8dSAndroid Build Coastguard Worker        # Presume that if .netrc has an entry, NNRP authentication is required.
947*cda5da8dSAndroid Build Coastguard Worker        try:
948*cda5da8dSAndroid Build Coastguard Worker            if usenetrc and not user:
949*cda5da8dSAndroid Build Coastguard Worker                import netrc
950*cda5da8dSAndroid Build Coastguard Worker                credentials = netrc.netrc()
951*cda5da8dSAndroid Build Coastguard Worker                auth = credentials.authenticators(self.host)
952*cda5da8dSAndroid Build Coastguard Worker                if auth:
953*cda5da8dSAndroid Build Coastguard Worker                    user = auth[0]
954*cda5da8dSAndroid Build Coastguard Worker                    password = auth[2]
955*cda5da8dSAndroid Build Coastguard Worker        except OSError:
956*cda5da8dSAndroid Build Coastguard Worker            pass
957*cda5da8dSAndroid Build Coastguard Worker        # Perform NNTP authentication if needed.
958*cda5da8dSAndroid Build Coastguard Worker        if not user:
959*cda5da8dSAndroid Build Coastguard Worker            return
960*cda5da8dSAndroid Build Coastguard Worker        resp = self._shortcmd('authinfo user ' + user)
961*cda5da8dSAndroid Build Coastguard Worker        if resp.startswith('381'):
962*cda5da8dSAndroid Build Coastguard Worker            if not password:
963*cda5da8dSAndroid Build Coastguard Worker                raise NNTPReplyError(resp)
964*cda5da8dSAndroid Build Coastguard Worker            else:
965*cda5da8dSAndroid Build Coastguard Worker                resp = self._shortcmd('authinfo pass ' + password)
966*cda5da8dSAndroid Build Coastguard Worker                if not resp.startswith('281'):
967*cda5da8dSAndroid Build Coastguard Worker                    raise NNTPPermanentError(resp)
968*cda5da8dSAndroid Build Coastguard Worker        # Capabilities might have changed after login
969*cda5da8dSAndroid Build Coastguard Worker        self._caps = None
970*cda5da8dSAndroid Build Coastguard Worker        self.getcapabilities()
971*cda5da8dSAndroid Build Coastguard Worker        # Attempt to send mode reader if it was requested after login.
972*cda5da8dSAndroid Build Coastguard Worker        # Only do so if we're not in reader mode already.
973*cda5da8dSAndroid Build Coastguard Worker        if self.readermode_afterauth and 'READER' not in self._caps:
974*cda5da8dSAndroid Build Coastguard Worker            self._setreadermode()
975*cda5da8dSAndroid Build Coastguard Worker            # Capabilities might have changed after MODE READER
976*cda5da8dSAndroid Build Coastguard Worker            self._caps = None
977*cda5da8dSAndroid Build Coastguard Worker            self.getcapabilities()
978*cda5da8dSAndroid Build Coastguard Worker
979*cda5da8dSAndroid Build Coastguard Worker    def _setreadermode(self):
980*cda5da8dSAndroid Build Coastguard Worker        try:
981*cda5da8dSAndroid Build Coastguard Worker            self.welcome = self._shortcmd('mode reader')
982*cda5da8dSAndroid Build Coastguard Worker        except NNTPPermanentError:
983*cda5da8dSAndroid Build Coastguard Worker            # Error 5xx, probably 'not implemented'
984*cda5da8dSAndroid Build Coastguard Worker            pass
985*cda5da8dSAndroid Build Coastguard Worker        except NNTPTemporaryError as e:
986*cda5da8dSAndroid Build Coastguard Worker            if e.response.startswith('480'):
987*cda5da8dSAndroid Build Coastguard Worker                # Need authorization before 'mode reader'
988*cda5da8dSAndroid Build Coastguard Worker                self.readermode_afterauth = True
989*cda5da8dSAndroid Build Coastguard Worker            else:
990*cda5da8dSAndroid Build Coastguard Worker                raise
991*cda5da8dSAndroid Build Coastguard Worker
992*cda5da8dSAndroid Build Coastguard Worker    if _have_ssl:
993*cda5da8dSAndroid Build Coastguard Worker        def starttls(self, context=None):
994*cda5da8dSAndroid Build Coastguard Worker            """Process a STARTTLS command. Arguments:
995*cda5da8dSAndroid Build Coastguard Worker            - context: SSL context to use for the encrypted connection
996*cda5da8dSAndroid Build Coastguard Worker            """
997*cda5da8dSAndroid Build Coastguard Worker            # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if
998*cda5da8dSAndroid Build Coastguard Worker            # a TLS session already exists.
999*cda5da8dSAndroid Build Coastguard Worker            if self.tls_on:
1000*cda5da8dSAndroid Build Coastguard Worker                raise ValueError("TLS is already enabled.")
1001*cda5da8dSAndroid Build Coastguard Worker            if self.authenticated:
1002*cda5da8dSAndroid Build Coastguard Worker                raise ValueError("TLS cannot be started after authentication.")
1003*cda5da8dSAndroid Build Coastguard Worker            resp = self._shortcmd('STARTTLS')
1004*cda5da8dSAndroid Build Coastguard Worker            if resp.startswith('382'):
1005*cda5da8dSAndroid Build Coastguard Worker                self.file.close()
1006*cda5da8dSAndroid Build Coastguard Worker                self.sock = _encrypt_on(self.sock, context, self.host)
1007*cda5da8dSAndroid Build Coastguard Worker                self.file = self.sock.makefile("rwb")
1008*cda5da8dSAndroid Build Coastguard Worker                self.tls_on = True
1009*cda5da8dSAndroid Build Coastguard Worker                # Capabilities may change after TLS starts up, so ask for them
1010*cda5da8dSAndroid Build Coastguard Worker                # again.
1011*cda5da8dSAndroid Build Coastguard Worker                self._caps = None
1012*cda5da8dSAndroid Build Coastguard Worker                self.getcapabilities()
1013*cda5da8dSAndroid Build Coastguard Worker            else:
1014*cda5da8dSAndroid Build Coastguard Worker                raise NNTPError("TLS failed to start.")
1015*cda5da8dSAndroid Build Coastguard Worker
1016*cda5da8dSAndroid Build Coastguard Worker
1017*cda5da8dSAndroid Build Coastguard Workerif _have_ssl:
1018*cda5da8dSAndroid Build Coastguard Worker    class NNTP_SSL(NNTP):
1019*cda5da8dSAndroid Build Coastguard Worker
1020*cda5da8dSAndroid Build Coastguard Worker        def __init__(self, host, port=NNTP_SSL_PORT,
1021*cda5da8dSAndroid Build Coastguard Worker                    user=None, password=None, ssl_context=None,
1022*cda5da8dSAndroid Build Coastguard Worker                    readermode=None, usenetrc=False,
1023*cda5da8dSAndroid Build Coastguard Worker                    timeout=_GLOBAL_DEFAULT_TIMEOUT):
1024*cda5da8dSAndroid Build Coastguard Worker            """This works identically to NNTP.__init__, except for the change
1025*cda5da8dSAndroid Build Coastguard Worker            in default port and the `ssl_context` argument for SSL connections.
1026*cda5da8dSAndroid Build Coastguard Worker            """
1027*cda5da8dSAndroid Build Coastguard Worker            self.ssl_context = ssl_context
1028*cda5da8dSAndroid Build Coastguard Worker            super().__init__(host, port, user, password, readermode,
1029*cda5da8dSAndroid Build Coastguard Worker                             usenetrc, timeout)
1030*cda5da8dSAndroid Build Coastguard Worker
1031*cda5da8dSAndroid Build Coastguard Worker        def _create_socket(self, timeout):
1032*cda5da8dSAndroid Build Coastguard Worker            sock = super()._create_socket(timeout)
1033*cda5da8dSAndroid Build Coastguard Worker            try:
1034*cda5da8dSAndroid Build Coastguard Worker                sock = _encrypt_on(sock, self.ssl_context, self.host)
1035*cda5da8dSAndroid Build Coastguard Worker            except:
1036*cda5da8dSAndroid Build Coastguard Worker                sock.close()
1037*cda5da8dSAndroid Build Coastguard Worker                raise
1038*cda5da8dSAndroid Build Coastguard Worker            else:
1039*cda5da8dSAndroid Build Coastguard Worker                return sock
1040*cda5da8dSAndroid Build Coastguard Worker
1041*cda5da8dSAndroid Build Coastguard Worker    __all__.append("NNTP_SSL")
1042*cda5da8dSAndroid Build Coastguard Worker
1043*cda5da8dSAndroid Build Coastguard Worker
1044*cda5da8dSAndroid Build Coastguard Worker# Test retrieval when run as a script.
1045*cda5da8dSAndroid Build Coastguard Workerif __name__ == '__main__':
1046*cda5da8dSAndroid Build Coastguard Worker    import argparse
1047*cda5da8dSAndroid Build Coastguard Worker
1048*cda5da8dSAndroid Build Coastguard Worker    parser = argparse.ArgumentParser(description="""\
1049*cda5da8dSAndroid Build Coastguard Worker        nntplib built-in demo - display the latest articles in a newsgroup""")
1050*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('-g', '--group', default='gmane.comp.python.general',
1051*cda5da8dSAndroid Build Coastguard Worker                        help='group to fetch messages from (default: %(default)s)')
1052*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('-s', '--server', default='news.gmane.io',
1053*cda5da8dSAndroid Build Coastguard Worker                        help='NNTP server hostname (default: %(default)s)')
1054*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('-p', '--port', default=-1, type=int,
1055*cda5da8dSAndroid Build Coastguard Worker                        help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT))
1056*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('-n', '--nb-articles', default=10, type=int,
1057*cda5da8dSAndroid Build Coastguard Worker                        help='number of articles to fetch (default: %(default)s)')
1058*cda5da8dSAndroid Build Coastguard Worker    parser.add_argument('-S', '--ssl', action='store_true', default=False,
1059*cda5da8dSAndroid Build Coastguard Worker                        help='use NNTP over SSL')
1060*cda5da8dSAndroid Build Coastguard Worker    args = parser.parse_args()
1061*cda5da8dSAndroid Build Coastguard Worker
1062*cda5da8dSAndroid Build Coastguard Worker    port = args.port
1063*cda5da8dSAndroid Build Coastguard Worker    if not args.ssl:
1064*cda5da8dSAndroid Build Coastguard Worker        if port == -1:
1065*cda5da8dSAndroid Build Coastguard Worker            port = NNTP_PORT
1066*cda5da8dSAndroid Build Coastguard Worker        s = NNTP(host=args.server, port=port)
1067*cda5da8dSAndroid Build Coastguard Worker    else:
1068*cda5da8dSAndroid Build Coastguard Worker        if port == -1:
1069*cda5da8dSAndroid Build Coastguard Worker            port = NNTP_SSL_PORT
1070*cda5da8dSAndroid Build Coastguard Worker        s = NNTP_SSL(host=args.server, port=port)
1071*cda5da8dSAndroid Build Coastguard Worker
1072*cda5da8dSAndroid Build Coastguard Worker    caps = s.getcapabilities()
1073*cda5da8dSAndroid Build Coastguard Worker    if 'STARTTLS' in caps:
1074*cda5da8dSAndroid Build Coastguard Worker        s.starttls()
1075*cda5da8dSAndroid Build Coastguard Worker    resp, count, first, last, name = s.group(args.group)
1076*cda5da8dSAndroid Build Coastguard Worker    print('Group', name, 'has', count, 'articles, range', first, 'to', last)
1077*cda5da8dSAndroid Build Coastguard Worker
1078*cda5da8dSAndroid Build Coastguard Worker    def cut(s, lim):
1079*cda5da8dSAndroid Build Coastguard Worker        if len(s) > lim:
1080*cda5da8dSAndroid Build Coastguard Worker            s = s[:lim - 4] + "..."
1081*cda5da8dSAndroid Build Coastguard Worker        return s
1082*cda5da8dSAndroid Build Coastguard Worker
1083*cda5da8dSAndroid Build Coastguard Worker    first = str(int(last) - args.nb_articles + 1)
1084*cda5da8dSAndroid Build Coastguard Worker    resp, overviews = s.xover(first, last)
1085*cda5da8dSAndroid Build Coastguard Worker    for artnum, over in overviews:
1086*cda5da8dSAndroid Build Coastguard Worker        author = decode_header(over['from']).split('<', 1)[0]
1087*cda5da8dSAndroid Build Coastguard Worker        subject = decode_header(over['subject'])
1088*cda5da8dSAndroid Build Coastguard Worker        lines = int(over[':lines'])
1089*cda5da8dSAndroid Build Coastguard Worker        print("{:7} {:20} {:42} ({})".format(
1090*cda5da8dSAndroid Build Coastguard Worker              artnum, cut(author, 20), cut(subject, 42), lines)
1091*cda5da8dSAndroid Build Coastguard Worker              )
1092*cda5da8dSAndroid Build Coastguard Worker
1093*cda5da8dSAndroid Build Coastguard Worker    s.quit()
1094