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