2024-11-23 22:40:06 -05:00
|
|
|
import re
|
2024-11-20 21:17:03 -05:00
|
|
|
import enum
|
|
|
|
import socket
|
2024-11-23 22:40:06 -05:00
|
|
|
import datetime
|
|
|
|
import fnmatch
|
2024-11-25 20:27:14 -05:00
|
|
|
import traceback
|
2024-11-27 12:36:02 -05:00
|
|
|
import email.header
|
|
|
|
import email.utils
|
2024-11-20 21:17:03 -05:00
|
|
|
|
2024-11-22 23:57:14 -05:00
|
|
|
from typing import Optional
|
|
|
|
|
2024-11-26 18:50:47 -05:00
|
|
|
from nntp.tiny.buffer import LineBuffer, OutputBuffer, BufferOverflow
|
2024-11-23 00:20:06 -05:00
|
|
|
from nntp.tiny.db import Database
|
2024-11-22 23:57:14 -05:00
|
|
|
from nntp.tiny.response import Response, ResponseCode
|
|
|
|
from nntp.tiny.newsgroup import Newsgroup
|
|
|
|
from nntp.tiny.message import Message
|
2024-11-20 21:17:03 -05:00
|
|
|
|
|
|
|
class SessionState(enum.Flag):
|
2024-11-26 10:27:03 -05:00
|
|
|
ACTIVE = 1
|
2024-11-20 21:17:03 -05:00
|
|
|
AUTH_OK = enum.auto()
|
|
|
|
AUTH_POST = enum.auto()
|
|
|
|
|
2024-11-26 10:44:21 -05:00
|
|
|
class SessionMode(enum.Enum):
|
|
|
|
READER = 1
|
|
|
|
|
2024-11-25 20:27:55 -05:00
|
|
|
class MessagePart(enum.Enum):
|
|
|
|
HEAD = 1
|
|
|
|
BODY = enum.auto()
|
|
|
|
WHOLE = enum.auto()
|
|
|
|
|
2024-11-22 23:57:14 -05:00
|
|
|
class MessageRange():
|
|
|
|
__slots__ = 'id', 'min', 'max',
|
|
|
|
|
2024-11-25 00:48:56 -05:00
|
|
|
RE_NUM = re.compile(r'^(\d+)$')
|
|
|
|
RE_RANGE = re.compile(r'^(\d+)-(\d+)$')
|
|
|
|
RE_RANGE_LOWER = re.compile(r'^(\d+$)-$')
|
|
|
|
RE_RANGE_UPPER = re.compile(r'^-(\d+$)$')
|
2024-11-22 23:57:14 -05:00
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.id: int = None
|
|
|
|
self.min: int = None
|
|
|
|
self.max: int = None
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
if self.id is not None:
|
|
|
|
return str(self.id)
|
|
|
|
|
|
|
|
if self.min is not None and self.max is None:
|
|
|
|
return "%d-" % (self.min)
|
|
|
|
elif self.min is not None and self.max is not None:
|
|
|
|
return "%d-%d" % (self.min, self.max)
|
|
|
|
elif self.min is None and self.max is not None:
|
|
|
|
return "-%d" % (self.max)
|
|
|
|
|
|
|
|
return "?"
|
|
|
|
|
2024-11-23 00:20:06 -05:00
|
|
|
def where(self):
|
2024-11-22 23:57:14 -05:00
|
|
|
if self.id is not None:
|
2024-11-23 00:20:06 -05:00
|
|
|
return "id = %d" % (self.id)
|
2024-11-22 23:57:14 -05:00
|
|
|
|
|
|
|
if self.min is not None and self.max is None:
|
2024-11-23 00:20:06 -05:00
|
|
|
return "id >= %d" % (self.min)
|
2024-11-22 23:57:14 -05:00
|
|
|
elif self.min is not None and self.max is not None:
|
2024-11-23 00:20:06 -05:00
|
|
|
return "id >= %d and id <= %d" % (self.min, self.max)
|
2024-11-22 23:57:14 -05:00
|
|
|
elif self.min is None and self.max is not None:
|
2024-11-23 00:20:06 -05:00
|
|
|
return "id <= %d" % (self.max)
|
2024-11-22 23:57:14 -05:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def parse(r: str):
|
|
|
|
match = __class__.RE_NUM.match(r)
|
|
|
|
if match:
|
|
|
|
obj = __class__()
|
2024-11-25 21:52:07 -05:00
|
|
|
obj.id = int(match[1])
|
2024-11-22 23:57:14 -05:00
|
|
|
return obj
|
|
|
|
|
|
|
|
match = __class__.RE_RANGE.match(r)
|
|
|
|
if match:
|
|
|
|
obj = __class__()
|
2024-11-25 21:52:07 -05:00
|
|
|
obj.min = int(match[1])
|
|
|
|
obj.max = int(match[2])
|
2024-11-22 23:57:14 -05:00
|
|
|
return obj
|
|
|
|
|
|
|
|
match = __class__.RE_RANGE_LOWER.match(r)
|
|
|
|
if match:
|
|
|
|
obj = __class__()
|
2024-11-25 21:52:07 -05:00
|
|
|
obj.min = int(match[1])
|
2024-11-22 23:57:14 -05:00
|
|
|
return obj
|
|
|
|
|
|
|
|
match = __class__.RE_RANGE_UPPER.match(r)
|
|
|
|
if match:
|
|
|
|
obj = __class__()
|
2024-11-25 21:52:07 -05:00
|
|
|
obj.max = int(match[1])
|
2024-11-22 23:57:14 -05:00
|
|
|
return obj
|
|
|
|
|
2024-11-20 21:17:03 -05:00
|
|
|
class Session():
|
2024-11-25 17:17:08 -05:00
|
|
|
NNTP_VERSION = 2
|
2024-11-20 21:17:03 -05:00
|
|
|
NNTP_CAPABILITIES = [
|
2024-11-25 00:16:15 -05:00
|
|
|
'VERSION %d' % (NNTP_VERSION),
|
2024-11-20 21:17:03 -05:00
|
|
|
'READER',
|
|
|
|
'HDR',
|
|
|
|
'NEWNEWS',
|
|
|
|
'LIST ACTIVE NEWSGROUP OVERVIEW.FMT SUBSCRIPTIONS',
|
|
|
|
'OVER MSGID'
|
|
|
|
]
|
|
|
|
|
2024-11-25 17:17:08 -05:00
|
|
|
RE_SPLIT = re.compile(r'\s+')
|
|
|
|
|
2024-11-26 16:55:44 -05:00
|
|
|
def __init__(self, server, sock: socket.socket):
|
2024-11-26 18:50:47 -05:00
|
|
|
self.server = server
|
|
|
|
self.db: Database = server.connect_to_db()
|
|
|
|
self.sock: socket.socket = sock
|
|
|
|
self.buf: LineBuffer = LineBuffer()
|
|
|
|
self.output: OutputBuffer = OutputBuffer(sock)
|
|
|
|
self.state: SessionState = SessionState.ACTIVE
|
|
|
|
self.mode: SessionMode = SessionMode.READER
|
2024-11-20 21:17:03 -05:00
|
|
|
|
2024-11-25 20:27:55 -05:00
|
|
|
self.newsgroup: Optional[Newsgroup] = None
|
|
|
|
self.article_id: Optional[int] = None
|
2024-11-22 23:57:14 -05:00
|
|
|
|
2024-11-20 21:17:03 -05:00
|
|
|
def readline(self):
|
|
|
|
return self.buf.readline(self.sock)
|
|
|
|
|
|
|
|
def print(self, text: str, end: str="\r\n"):
|
2024-11-26 18:50:47 -05:00
|
|
|
return self.output.print(text, end)
|
|
|
|
|
|
|
|
def flush(self):
|
|
|
|
return self.output.flush()
|
2024-11-20 21:17:03 -05:00
|
|
|
|
|
|
|
def end(self):
|
|
|
|
return self.print('.')
|
|
|
|
|
|
|
|
def respond(self, code: ResponseCode, message: str=None, body=None):
|
|
|
|
response = Response(code, message, body)
|
|
|
|
|
|
|
|
return self.print(str(response))
|
|
|
|
|
|
|
|
def _cmd_capabilities(self, *args):
|
|
|
|
self.respond(ResponseCode.NNTP_CAPABILITIES_FOLLOW)
|
|
|
|
|
|
|
|
if self.state & SessionState.AUTH_POST:
|
|
|
|
self.print('POST')
|
|
|
|
|
|
|
|
if self.state & SessionState.AUTH_OK:
|
|
|
|
self.print('AUTHUSER INFO')
|
|
|
|
|
|
|
|
for item in self.NNTP_CAPABILITIES:
|
|
|
|
self.print(item)
|
|
|
|
|
|
|
|
self.end()
|
|
|
|
|
2024-11-26 10:44:21 -05:00
|
|
|
def _cmd_mode(self, *args):
|
|
|
|
if len(args) != 1 or args[0] != 'READER':
|
|
|
|
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR)
|
|
|
|
|
|
|
|
self.mode = SessionMode.READER
|
|
|
|
|
|
|
|
return self.respond(ResponseCode.NNTP_POST_PROHIBITED)
|
|
|
|
|
2024-11-22 23:57:14 -05:00
|
|
|
def _cmd_group(self, name: str):
|
|
|
|
if name not in self.server.newsgroups:
|
|
|
|
return self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_FOUND)
|
|
|
|
|
|
|
|
newsgroup = self.server.newsgroups[name]
|
|
|
|
|
|
|
|
sql = """
|
|
|
|
select
|
|
|
|
count(id),
|
|
|
|
min(id),
|
|
|
|
max(id)
|
|
|
|
from
|
|
|
|
newsgroup_message
|
|
|
|
where
|
|
|
|
newsgroup_id = ?
|
|
|
|
"""
|
|
|
|
|
2024-11-25 00:49:24 -05:00
|
|
|
cr = self.db.execute(sql, (newsgroup.id,))
|
2024-11-22 23:57:14 -05:00
|
|
|
row = cr.fetchone()
|
|
|
|
|
2024-11-26 11:26:19 -05:00
|
|
|
if row is None:
|
|
|
|
text = "%d %d %d %s" % (
|
|
|
|
0, 0, 0, newsgroup.name
|
|
|
|
)
|
|
|
|
|
|
|
|
self.article_id = None
|
|
|
|
else:
|
|
|
|
text = "%d %d %d %s" % (
|
|
|
|
row[0],
|
|
|
|
row[1],
|
|
|
|
row[2],
|
|
|
|
newsgroup.name
|
|
|
|
)
|
2024-11-22 23:57:14 -05:00
|
|
|
|
2024-11-26 11:26:19 -05:00
|
|
|
self.article_id = row[1]
|
2024-11-22 23:57:14 -05:00
|
|
|
|
2024-11-26 11:26:19 -05:00
|
|
|
self.newsgroup = newsgroup
|
2024-11-22 23:57:14 -05:00
|
|
|
|
2024-11-26 11:26:19 -05:00
|
|
|
return self.respond(ResponseCode.NNTP_GROUP_LISTING, text)
|
2024-11-22 23:57:14 -05:00
|
|
|
|
2024-11-26 12:08:56 -05:00
|
|
|
def _cmd_last(self):
|
|
|
|
if self.newsgroup is None:
|
|
|
|
return self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
|
|
|
|
|
|
|
|
if self.article_id is None:
|
|
|
|
return self.respond(ResponseCode.NNTP_ARTICLE_INVALID_NUMBER)
|
|
|
|
|
|
|
|
sql = """
|
|
|
|
select
|
|
|
|
max(id)
|
|
|
|
from
|
|
|
|
newsgroup_message
|
|
|
|
where
|
|
|
|
newsgroup_id = ?
|
|
|
|
and id < ?
|
|
|
|
"""
|
|
|
|
|
|
|
|
cr = self.db.execute(sql, (self.newsgroup.id, self.article_id))
|
|
|
|
row = cr.fetchone()
|
|
|
|
|
|
|
|
if row is None or row[0] is None:
|
|
|
|
return self.respond(ResponseCode.NNTP_ARTICLE_NO_PREVIOUS)
|
|
|
|
|
|
|
|
self.article_id = row[0]
|
|
|
|
|
|
|
|
return self.respond(ResponseCode.NNTP_ARTICLE_STAT_RESPONSE)
|
|
|
|
|
|
|
|
def _cmd_next(self):
|
|
|
|
if self.newsgroup is None:
|
|
|
|
return self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
|
|
|
|
|
|
|
|
if self.article_id is None:
|
|
|
|
return self.respond(ResponseCode.NNTP_ARTICLE_INVALID_NUMBER)
|
|
|
|
|
|
|
|
sql = """
|
|
|
|
select
|
|
|
|
min(id)
|
|
|
|
from
|
|
|
|
newsgroup_message
|
|
|
|
where
|
|
|
|
newsgroup_id = ?
|
|
|
|
and id > ?
|
|
|
|
"""
|
|
|
|
|
|
|
|
cr = self.db.execute(sql, (self.newsgroup.id, self.article_id))
|
|
|
|
row = cr.fetchone()
|
|
|
|
|
|
|
|
if row is None or row[0] is None:
|
|
|
|
return self.respond(ResponseCode.NNTP_ARTICLE_NO_NEXT)
|
|
|
|
|
|
|
|
self.article_id = row[0]
|
|
|
|
|
|
|
|
return self.respond(ResponseCode.NNTP_ARTICLE_STAT_RESPONSE)
|
|
|
|
|
2024-11-23 00:20:06 -05:00
|
|
|
def _newsgroup_summary(self, newsgroup: Newsgroup) -> str:
|
|
|
|
sql = """
|
|
|
|
select
|
|
|
|
count(id),
|
|
|
|
min(id),
|
|
|
|
max(id)
|
|
|
|
from
|
|
|
|
newsgroup_message
|
|
|
|
where
|
|
|
|
newsgroup_id = ?
|
|
|
|
"""
|
|
|
|
|
|
|
|
cr = self.db.execute(sql, (newsgroup.id))
|
|
|
|
row = cr.fetchone()
|
|
|
|
|
|
|
|
return "%d %d %d %s" % (
|
|
|
|
row[0],
|
|
|
|
row[1],
|
|
|
|
row[2],
|
|
|
|
newsgroup.name
|
|
|
|
)
|
2024-11-22 23:57:14 -05:00
|
|
|
|
|
|
|
def _cmd_listgroup(self, *args):
|
|
|
|
newsgroup = self.newsgroup
|
2024-11-25 17:18:22 -05:00
|
|
|
|
2024-11-22 23:57:14 -05:00
|
|
|
if len(args) == 0 and newsgroup is None:
|
|
|
|
return self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
|
|
|
|
elif len(args) > 0:
|
|
|
|
newsgroup = self.server.newsgroups.get(args[0])
|
|
|
|
|
|
|
|
if newsgroup is None:
|
|
|
|
return self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_FOUND)
|
|
|
|
|
2024-11-23 00:20:06 -05:00
|
|
|
sql = """
|
|
|
|
select
|
|
|
|
id
|
|
|
|
from
|
|
|
|
newsgroup_message
|
|
|
|
where
|
|
|
|
newsgroup_id = ?
|
|
|
|
"""
|
2024-11-22 23:57:14 -05:00
|
|
|
|
|
|
|
if len(args) > 1:
|
2024-11-23 00:20:06 -05:00
|
|
|
msgrange = MessageRange.parse(args[1])
|
|
|
|
|
2024-11-25 21:52:27 -05:00
|
|
|
sql += " and " + msgrange.where()
|
2024-11-22 23:57:14 -05:00
|
|
|
|
2024-11-23 00:22:24 -05:00
|
|
|
summary = self._newsgroup_summary(newsgroup)
|
|
|
|
cr = self.db.execute(sql, (newsgroup.id))
|
2024-11-22 23:57:14 -05:00
|
|
|
|
2024-11-23 00:22:24 -05:00
|
|
|
self.respond(ResponseCode.NNTP_GROUP_LISTING, summary)
|
2024-11-22 23:57:14 -05:00
|
|
|
|
|
|
|
for message in cr.each():
|
|
|
|
self.print(str(message.id))
|
|
|
|
|
|
|
|
return self.end()
|
|
|
|
|
2024-11-25 15:53:32 -05:00
|
|
|
def _newsgroup_summary(self, newsgroup: Newsgroup):
|
|
|
|
sql = """
|
|
|
|
select
|
|
|
|
min(id),
|
|
|
|
max(id)
|
|
|
|
from
|
|
|
|
newsgroup_message
|
|
|
|
where
|
|
|
|
newsgroup_id = ?
|
|
|
|
"""
|
|
|
|
|
|
|
|
cr = self.db.execute(sql, (newsgroup.id,))
|
|
|
|
row = cr.fetchone()
|
|
|
|
|
|
|
|
return {
|
|
|
|
'low': row[0],
|
|
|
|
'high': row[1],
|
|
|
|
'perms': 'n'
|
|
|
|
}
|
|
|
|
|
|
|
|
def print_newsgroup(self, newsgroup: Newsgroup):
|
|
|
|
summary = self._newsgroup_summary(newsgroup)
|
|
|
|
|
|
|
|
return self.print("%s %d %d %s" % (
|
|
|
|
newsgroup.name,
|
|
|
|
summary['low'],
|
|
|
|
summary['high'],
|
|
|
|
summary['perms']
|
|
|
|
))
|
|
|
|
|
|
|
|
def _cmd_list_newsgroups(self):
|
|
|
|
self.respond(ResponseCode.NNTP_INFORMATION_FOLLOWS)
|
|
|
|
|
|
|
|
for name in self.server.newsgroups:
|
|
|
|
newsgroup = self.server.newsgroups[name]
|
|
|
|
|
|
|
|
self.print_newsgroup(newsgroup)
|
|
|
|
|
|
|
|
return self.end()
|
|
|
|
|
2024-11-25 17:18:38 -05:00
|
|
|
def _newsgroup_last_active(self, newsgroup: Newsgroup):
|
|
|
|
sql = """
|
|
|
|
select
|
|
|
|
max(created_on)
|
|
|
|
from
|
|
|
|
newsgroup_message
|
|
|
|
where
|
|
|
|
newsgroup_id = ?
|
|
|
|
"""
|
|
|
|
|
|
|
|
cr = self.db.execute(sql, (newsgroup.id,))
|
|
|
|
row = cr.fetchone()
|
|
|
|
|
|
|
|
if row is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
return datetime.datetime.fromisoformat(row[0])
|
|
|
|
|
|
|
|
def _cmd_list_active(self):
|
|
|
|
now = datetime.datetime.now(datetime.UTC)
|
|
|
|
|
|
|
|
self.respond(ResponseCode.NNTP_INFORMATION_FOLLOWS)
|
|
|
|
|
|
|
|
for name in self.server.newsgroups:
|
|
|
|
newsgroup = self.server.newsgroups[name]
|
|
|
|
last_active = self._newsgroup_last_active(newsgroup)
|
|
|
|
|
|
|
|
if now - last_active < datetime.timedelta(days=1):
|
|
|
|
self.print_newsgroup(newsgroup)
|
|
|
|
|
|
|
|
return self.end()
|
|
|
|
|
2024-11-26 14:05:34 -05:00
|
|
|
def _cmd_list_active_times(self):
|
|
|
|
self.respond(ResponseCode.NNTP_INFORMATION_FOLLOWS)
|
|
|
|
|
|
|
|
for name in self.server.newsgroups:
|
|
|
|
newsgroup = self.server.newsgroups[name]
|
|
|
|
|
|
|
|
self.print("%s %d %s" % (
|
|
|
|
name,
|
|
|
|
newsgroup.created_on.timestamp(),
|
|
|
|
newsgroup.created_by
|
|
|
|
))
|
|
|
|
|
|
|
|
return self.end()
|
|
|
|
|
2024-11-26 13:28:33 -05:00
|
|
|
OVERVIEW_FMT_HEADERS = [
|
|
|
|
'Subject',
|
|
|
|
'From',
|
|
|
|
'Date',
|
|
|
|
'Message-ID',
|
|
|
|
'References',
|
|
|
|
'Bytes',
|
|
|
|
'Lines',
|
|
|
|
]
|
|
|
|
|
|
|
|
def _cmd_list_overview_fmt(self):
|
|
|
|
self.respond(ResponseCode.NNTP_INFORMATION_FOLLOWS, "Order of fields in overview database")
|
|
|
|
|
|
|
|
for header in self.OVERVIEW_FMT_HEADERS:
|
|
|
|
self.print("%s:" % (header,))
|
|
|
|
|
|
|
|
return self.end()
|
|
|
|
|
2024-11-26 16:02:54 -05:00
|
|
|
SUPPORTED_HEADERS = [
|
|
|
|
':',
|
|
|
|
':lines',
|
|
|
|
':bytes',
|
|
|
|
]
|
|
|
|
|
|
|
|
def _cmd_list_headers(self):
|
|
|
|
self.respond(ResponseCode.NNTP_INFORMATION_FOLLOWS, "metadata items supported")
|
|
|
|
|
|
|
|
for name in self.SUPPORTED_HEADERS:
|
|
|
|
self.print(name)
|
|
|
|
|
|
|
|
self.end()
|
|
|
|
|
2024-11-25 15:53:32 -05:00
|
|
|
LIST_SUBCOMMANDS = {
|
2024-11-26 13:28:33 -05:00
|
|
|
'NEWSGROUPS': _cmd_list_newsgroups,
|
|
|
|
'ACTIVE': _cmd_list_active,
|
2024-11-26 14:05:34 -05:00
|
|
|
'ACTIVE.TIMES': _cmd_list_active_times,
|
2024-11-26 13:28:33 -05:00
|
|
|
'OVERVIEW.FMT': _cmd_list_overview_fmt,
|
2024-11-26 16:02:54 -05:00
|
|
|
'HEADERS': _cmd_list_headers,
|
2024-11-25 15:53:32 -05:00
|
|
|
}
|
|
|
|
|
2024-11-23 22:40:06 -05:00
|
|
|
def _cmd_list(self, *args):
|
2024-11-25 15:53:32 -05:00
|
|
|
if len(args) == 0:
|
|
|
|
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR, "No subcommand provided")
|
|
|
|
|
|
|
|
subcmd, *subargs = args
|
|
|
|
|
|
|
|
fn = self.LIST_SUBCOMMANDS.get(subcmd.upper())
|
|
|
|
|
|
|
|
if fn is None:
|
|
|
|
return self.respond(ResponseCode.NNTP_COMMAND_UNKNOWN)
|
|
|
|
|
|
|
|
return fn(self, *subargs)
|
2024-11-23 22:40:06 -05:00
|
|
|
|
|
|
|
RE_DATE_SHORT = re.compile(r'^(\d{2})(\d{2})(\d{2})$')
|
|
|
|
RE_DATE_LONG = re.compile(r'^(\d{4})(\d{2})(\d{2})$')
|
|
|
|
RE_TIME = re.compile(r'^(\d{2})(\d{2})(\d{2})$')
|
|
|
|
|
|
|
|
def _parse_date_time(self, datestr: str, timestr: str):
|
|
|
|
yyyy, mm, dd = None, None, None,
|
|
|
|
hh, MM, ss = None, None, None
|
2024-11-25 17:18:22 -05:00
|
|
|
|
2024-11-23 22:40:06 -05:00
|
|
|
match = self.RE_DATE_SHORT.match(datestr)
|
|
|
|
if match:
|
|
|
|
yy, mm, dd = map(int, match[1:3])
|
|
|
|
|
|
|
|
if yy >= 70:
|
|
|
|
yyyy = 1900 + yy
|
|
|
|
else:
|
|
|
|
yyyy = 2000 + yy
|
|
|
|
|
|
|
|
match = self.RE_DATE_LONG.match(datestr)
|
|
|
|
if match:
|
|
|
|
yyyy, mm, dd = map(int, match[1:3])
|
|
|
|
|
|
|
|
if yyyy is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
match = self.RE_TIME.match(timestr)
|
|
|
|
if match is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
hh, mm, ss = map(int, match[1:3])
|
|
|
|
|
|
|
|
return datetime.datetime(yyyy, mm, dd, hh, MM, ss)
|
|
|
|
|
|
|
|
def _cmd_newnews(self, wildmat, datestr, timestr, *args):
|
|
|
|
gmt = False
|
|
|
|
|
|
|
|
if len(args) == 1:
|
|
|
|
if args[0] == "GMT":
|
|
|
|
gmt = True
|
|
|
|
else:
|
|
|
|
return self.send_response(ResponseCode.NNTP_SYNTAX_ERROR, "Only optional 'GMT' allowed")
|
|
|
|
elif len(args) > 1:
|
|
|
|
return self.send_response(ResponseCode.NNTP_SYNTAX_ERROR, "Too many arguments")
|
|
|
|
|
|
|
|
timestamp = self._parse_date_time(datestr, timestr)
|
|
|
|
|
|
|
|
if timestamp is None:
|
|
|
|
return self.send_response(ResponseCode.NNTP_SYNTAX_ERROR, "Invalid date or time")
|
|
|
|
|
|
|
|
self.respond(ResponseCode.NNTP_ARTICLE_LISTING_ID_FOLLOWS)
|
|
|
|
|
|
|
|
sql = """
|
2024-11-26 11:25:40 -05:00
|
|
|
select
|
|
|
|
message_id
|
|
|
|
from
|
|
|
|
newsgroup_message
|
|
|
|
where
|
|
|
|
newsgroup_id = ?
|
|
|
|
and created_on >= ?
|
2024-11-23 22:40:06 -05:00
|
|
|
"""
|
|
|
|
|
|
|
|
for name in self.server.newsgroups:
|
|
|
|
if fnmatch.fnmatch(name, wildmat):
|
|
|
|
newsgroup = self.server.newsgroups[name]
|
|
|
|
|
|
|
|
cr = self.db.execute(sql, (newsgroup.id, timestamp.isoformat()))
|
|
|
|
|
|
|
|
for row in cr.each():
|
|
|
|
self.print(row[0])
|
|
|
|
|
|
|
|
return self.end()
|
|
|
|
|
2024-11-25 00:49:53 -05:00
|
|
|
def _cmd_newgroups(self, wildmat, datestr, timestr, *args):
|
|
|
|
gmt = False
|
|
|
|
|
|
|
|
if len(args) == 1:
|
|
|
|
if args[0] == "GMT":
|
|
|
|
gmt = True
|
|
|
|
else:
|
2024-11-25 14:25:55 -05:00
|
|
|
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR, "Only optional 'GMT' allowed")
|
2024-11-25 00:49:53 -05:00
|
|
|
elif len(args) > 1:
|
2024-11-25 14:25:55 -05:00
|
|
|
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR, "Too many arguments")
|
2024-11-25 00:49:53 -05:00
|
|
|
|
2024-11-25 14:25:55 -05:00
|
|
|
self.respond(ResponseCode.NNTP_GROUPS_NEW_FOLLOW)
|
|
|
|
|
|
|
|
for name in self.server.newsgroups:
|
|
|
|
if fnmatch.fnmatch(name, wildmat):
|
|
|
|
newsgroup = self.server.newsgroups[name]
|
2024-11-25 15:53:32 -05:00
|
|
|
|
|
|
|
self.print_newsgroup(newsgroup)
|
2024-11-25 14:25:55 -05:00
|
|
|
|
|
|
|
return self.end()
|
2024-11-25 00:49:53 -05:00
|
|
|
|
2024-11-27 00:22:12 -05:00
|
|
|
def _each_message_by_id(self, identifier: str, success: ResponseCode=ResponseCode.NNTP_INFORMATION_FOLLOWS):
|
2024-11-26 14:53:16 -05:00
|
|
|
if identifier is None:
|
|
|
|
if self.newsgroup is None:
|
|
|
|
self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
|
|
|
|
return
|
|
|
|
|
|
|
|
if self.article_id is None:
|
|
|
|
self.respond(ResponseCode.NNTP_ARTICLE_INVALID_NUMBER)
|
|
|
|
return
|
|
|
|
|
2024-11-26 15:49:54 -05:00
|
|
|
message = self.db.get(Message, {'id': str(self.article_id)})
|
2024-11-26 14:53:16 -05:00
|
|
|
|
|
|
|
if message is None:
|
|
|
|
self.respond(ResponseCode.NNTP_ARTICLE_INVALID_NUMBER)
|
|
|
|
return
|
|
|
|
|
2024-11-27 00:22:12 -05:00
|
|
|
self.respond(success)
|
2024-11-26 14:53:16 -05:00
|
|
|
|
|
|
|
yield message
|
|
|
|
elif identifier[0] == '<':
|
|
|
|
message = self.db.query(Message, {
|
|
|
|
'message_id': identifier
|
|
|
|
}).fetchone()
|
|
|
|
|
|
|
|
if message is None:
|
|
|
|
self.respond(ResponseCode.NNTP_ARTICLE_NOT_FOUND_ID)
|
|
|
|
return
|
|
|
|
|
2024-11-27 00:22:12 -05:00
|
|
|
self.respond(success)
|
2024-11-26 14:53:16 -05:00
|
|
|
|
|
|
|
yield message
|
2024-11-25 20:27:55 -05:00
|
|
|
else:
|
2024-11-26 14:53:16 -05:00
|
|
|
if self.newsgroup is None:
|
|
|
|
self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
|
|
|
|
return
|
|
|
|
|
|
|
|
msgrange = MessageRange.parse(identifier)
|
|
|
|
|
|
|
|
sql = f"select * from {Message.name} where "
|
|
|
|
sql += " newsgroup_id = ? and " + msgrange.where()
|
|
|
|
|
|
|
|
cr = self.db.query_sql(Message, sql, (self.newsgroup.id,))
|
|
|
|
|
|
|
|
first = True
|
|
|
|
|
|
|
|
for message in cr.each():
|
|
|
|
if first:
|
|
|
|
first = False
|
2024-11-27 00:22:12 -05:00
|
|
|
self.respond(success)
|
2024-11-26 14:53:16 -05:00
|
|
|
|
|
|
|
yield message
|
|
|
|
|
|
|
|
if first:
|
|
|
|
self.respond(ResponseCode.NNTP_ARTICLE_NOT_FOUND_RANGE)
|
|
|
|
|
|
|
|
self.end()
|
2024-11-25 20:27:55 -05:00
|
|
|
|
|
|
|
def _send_message_headers(self, message: Message):
|
|
|
|
for name in message.headers:
|
|
|
|
self.print("%s: %s" % (
|
|
|
|
name, message.headers[name]
|
|
|
|
))
|
|
|
|
|
2024-11-26 15:49:54 -05:00
|
|
|
def _message_by_id(self, identifier: Optional[str]=None):
|
|
|
|
if identifier is None:
|
|
|
|
if self.newsgroup is None:
|
|
|
|
self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
|
|
|
|
return
|
2024-11-25 20:27:55 -05:00
|
|
|
|
2024-11-26 15:49:54 -05:00
|
|
|
if self.article_id is None:
|
|
|
|
self.respond(ResponseCode.NNTP_ARTICLE_INVALID_NUMBER)
|
|
|
|
return
|
2024-11-25 20:27:55 -05:00
|
|
|
|
2024-11-26 15:49:54 -05:00
|
|
|
message = self.db.get(Message, {'id': self.article_id})
|
|
|
|
|
|
|
|
if message is None:
|
|
|
|
self.respond(ResponseCode.NNTP_ARTICLE_NOT_FOUND_NUM)
|
|
|
|
return
|
|
|
|
|
|
|
|
return message
|
|
|
|
elif identifier[0] == '<':
|
|
|
|
message = self.db.query(Message, {
|
|
|
|
'message_id': identifier
|
|
|
|
}).fetchone()
|
|
|
|
|
|
|
|
if message is None:
|
|
|
|
self.respond(ResponseCode.NNTP_ARTICLE_NOT_FOUND_ID)
|
|
|
|
return
|
|
|
|
|
|
|
|
return message
|
2024-11-25 20:27:55 -05:00
|
|
|
else:
|
2024-11-26 15:49:54 -05:00
|
|
|
message = self.db.get(Message, {'id': int(identifier)})
|
|
|
|
|
|
|
|
if message is None:
|
|
|
|
self.respond(ResponseCode.NNTP_ARTICLE_NOT_FOUND_NUM)
|
|
|
|
return
|
|
|
|
|
|
|
|
return message
|
|
|
|
|
|
|
|
def _serve_message(self, part: MessagePart, identifier: Optional[str]=None):
|
|
|
|
message = self._message_by_id(identifier)
|
|
|
|
|
|
|
|
if message is None:
|
|
|
|
return
|
2024-11-25 20:27:55 -05:00
|
|
|
|
|
|
|
text = "%d %s" % (
|
|
|
|
message.id,
|
|
|
|
message.message_id
|
|
|
|
)
|
|
|
|
|
|
|
|
self.respond(ResponseCode.NNTP_ARTICLE_LISTING, text)
|
|
|
|
|
|
|
|
if part is MessagePart.HEAD or part is MessagePart.WHOLE:
|
|
|
|
self._send_message_headers(message)
|
|
|
|
|
|
|
|
if part is MessagePart.WHOLE:
|
|
|
|
self.print('')
|
|
|
|
|
|
|
|
if part is MessagePart.BODY or part is MessagePart.WHOLE:
|
|
|
|
self.print(message.body)
|
|
|
|
|
|
|
|
return self.end()
|
|
|
|
|
2024-11-25 20:34:26 -05:00
|
|
|
def _cmd_head(self, identifier: Optional[str]=None):
|
2024-11-25 20:27:55 -05:00
|
|
|
return self._serve_message(MessagePart.HEAD, identifier)
|
|
|
|
|
2024-11-25 20:34:26 -05:00
|
|
|
def _cmd_body(self, identifier: Optional[str]=None):
|
2024-11-25 20:27:55 -05:00
|
|
|
return self._serve_message(MessagePart.BODY, identifier)
|
|
|
|
|
2024-11-25 20:34:26 -05:00
|
|
|
def _cmd_article(self, identifier: Optional[str]=None):
|
2024-11-25 20:27:55 -05:00
|
|
|
return self._serve_message(MessagePart.WHOLE, identifier)
|
|
|
|
|
2024-11-25 21:53:08 -05:00
|
|
|
def _send_message_header(self, message: Message, name: str):
|
|
|
|
return self.print("%d %s" % (
|
|
|
|
message.id, message.headers.get(name, '')
|
|
|
|
))
|
|
|
|
|
2024-11-26 14:53:16 -05:00
|
|
|
def _cmd_hdr(self, name: str, identifier: Optional[str]=None):
|
|
|
|
for message in self._each_message_by_id(identifier):
|
2024-11-25 21:53:08 -05:00
|
|
|
self._send_message_header(message, name)
|
|
|
|
|
2024-11-26 13:12:42 -05:00
|
|
|
def _message_overview(self, message: Message) -> dict:
|
2024-11-27 15:27:35 -05:00
|
|
|
def f(s: str):
|
|
|
|
return s.replace('\t', ' ').replace('\r', '').replace('\n', ' ').replace('\0', '')
|
|
|
|
|
|
|
|
parts = [
|
2024-11-26 13:12:42 -05:00
|
|
|
str(message.id),
|
2024-11-27 12:36:02 -05:00
|
|
|
email.header.Header(message.subject).encode(),
|
|
|
|
email.header.Header(message.sender).encode(),
|
|
|
|
email.utils.format_datetime(message.created_on),
|
2024-11-26 13:12:42 -05:00
|
|
|
message.message_id,
|
|
|
|
message.parent_id or '',
|
2024-11-27 00:06:12 -05:00
|
|
|
str(len(message.content)),
|
|
|
|
str(message.content.count('\n') + 1),
|
2024-11-27 15:27:35 -05:00
|
|
|
]
|
|
|
|
|
|
|
|
for k in message.headers:
|
|
|
|
parts.append("%s: %s" % (
|
|
|
|
k, message.headers[k]
|
|
|
|
))
|
|
|
|
|
|
|
|
return map(f, parts)
|
2024-11-26 13:12:42 -05:00
|
|
|
|
|
|
|
def _cmd_over(self, identifier: Optional[str]=None):
|
2024-11-27 00:22:12 -05:00
|
|
|
for message in self._each_message_by_id(identifier, ResponseCode.NNTP_OVERVIEW_FOLLOWS):
|
2024-11-26 14:53:16 -05:00
|
|
|
overview = self._message_overview(message)
|
2024-11-26 13:12:42 -05:00
|
|
|
|
2024-11-26 17:20:13 -05:00
|
|
|
self.print('\t'.join(overview))
|
2024-11-26 13:12:42 -05:00
|
|
|
|
2024-11-25 22:01:49 -05:00
|
|
|
def _cmd_stat(self, identifier: Optional[str]=None):
|
2024-11-26 15:49:54 -05:00
|
|
|
message = self._message_by_id(identifier)
|
2024-11-25 22:01:49 -05:00
|
|
|
|
2024-11-26 15:49:54 -05:00
|
|
|
if message is None:
|
|
|
|
return
|
2024-11-25 22:01:49 -05:00
|
|
|
|
2024-11-26 15:49:54 -05:00
|
|
|
text = "%d %s" % (message.id, message.message_id)
|
2024-11-25 22:01:49 -05:00
|
|
|
|
2024-11-26 15:49:54 -05:00
|
|
|
self.article_id = message.id
|
2024-11-25 22:01:49 -05:00
|
|
|
|
|
|
|
return self.respond(ResponseCode.NNTP_ARTICLE_STAT_RESPONSE, text)
|
|
|
|
|
2024-11-26 10:19:02 -05:00
|
|
|
def _cmd_date(self):
|
|
|
|
timestamp = datetime.datetime.now(datetime.UTC)
|
|
|
|
|
|
|
|
return self.respond(ResponseCode.NNTP_DATE,
|
|
|
|
timestamp.strftime("%Y%m%d%H%M%S"))
|
|
|
|
|
2024-11-26 10:27:03 -05:00
|
|
|
def _cmd_quit(self):
|
|
|
|
self.state &= ~SessionState.ACTIVE
|
|
|
|
|
|
|
|
return self.respond(ResponseCode.NNTP_CONNECTION_CLOSING)
|
|
|
|
|
2024-11-25 00:49:53 -05:00
|
|
|
COMMANDS = {
|
|
|
|
'CAPABILITIES': _cmd_capabilities,
|
2024-11-26 10:44:21 -05:00
|
|
|
'MODE': _cmd_mode,
|
2024-11-25 00:49:53 -05:00
|
|
|
'GROUP': _cmd_group,
|
2024-11-26 12:08:56 -05:00
|
|
|
'LAST': _cmd_last,
|
|
|
|
'NEXT': _cmd_next,
|
2024-11-25 00:49:53 -05:00
|
|
|
'LISTGROUP': _cmd_listgroup,
|
|
|
|
'LIST': _cmd_list,
|
|
|
|
'NEWNEWS': _cmd_newnews,
|
2024-11-25 14:25:55 -05:00
|
|
|
'NEWGROUPS': _cmd_newgroups,
|
2024-11-25 20:27:55 -05:00
|
|
|
'HEAD': _cmd_head,
|
|
|
|
'BODY': _cmd_body,
|
2024-11-25 21:53:08 -05:00
|
|
|
'ARTICLE': _cmd_article,
|
|
|
|
'HDR': _cmd_hdr,
|
2024-11-26 12:15:56 -05:00
|
|
|
'XHDR': _cmd_hdr,
|
2024-11-26 13:12:42 -05:00
|
|
|
'OVER': _cmd_over,
|
2024-11-26 17:20:22 -05:00
|
|
|
'XOVER': _cmd_over,
|
2024-11-25 22:01:49 -05:00
|
|
|
'STAT': _cmd_stat,
|
2024-11-26 10:19:02 -05:00
|
|
|
'DATE': _cmd_date,
|
2024-11-26 10:27:03 -05:00
|
|
|
'QUIT': _cmd_quit,
|
2024-11-25 00:49:53 -05:00
|
|
|
}
|
|
|
|
|
2024-11-26 12:18:51 -05:00
|
|
|
def greet(self):
|
2024-11-26 22:21:20 -05:00
|
|
|
self.respond(ResponseCode.NNTP_SERVICE_READY_POST_PROHIBITED)
|
|
|
|
self.flush()
|
2024-11-26 12:18:51 -05:00
|
|
|
|
2024-11-26 16:55:44 -05:00
|
|
|
def handle_command(self):
|
2024-11-20 21:17:03 -05:00
|
|
|
line = self.readline()
|
|
|
|
|
|
|
|
if line == '':
|
2024-11-26 18:45:26 -05:00
|
|
|
self.state &= ~SessionState.ACTIVE
|
2024-11-20 21:17:03 -05:00
|
|
|
return
|
|
|
|
|
2024-11-25 00:49:53 -05:00
|
|
|
tokens = self.RE_SPLIT.split(line.rstrip())
|
|
|
|
command, *args = tokens
|
|
|
|
|
|
|
|
fn = self.COMMANDS.get(command.upper())
|
|
|
|
|
|
|
|
if fn is None:
|
|
|
|
return self.respond(ResponseCode.NNTP_COMMAND_UNKNOWN)
|
|
|
|
|
2024-11-25 14:27:58 -05:00
|
|
|
try:
|
|
|
|
return fn(self, *args)
|
|
|
|
except TypeError as e:
|
2024-11-25 20:27:14 -05:00
|
|
|
traceback.print_exception(e)
|
2024-11-25 14:27:58 -05:00
|
|
|
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR)
|
|
|
|
except Exception as e:
|
2024-11-25 20:27:14 -05:00
|
|
|
traceback.print_exception(e)
|
2024-11-25 14:27:58 -05:00
|
|
|
return self.respond(ResponseCode.NNTP_COMMAND_UNAVAILABLE)
|
2024-11-26 16:55:44 -05:00
|
|
|
|
|
|
|
def handle(self):
|
|
|
|
self.greet()
|
|
|
|
|
|
|
|
while self.state & SessionState.ACTIVE:
|
|
|
|
self.handle_command()
|
2024-11-26 18:50:47 -05:00
|
|
|
self.flush()
|
2024-11-26 16:55:44 -05:00
|
|
|
|
|
|
|
self.sock.close()
|