# Copyright (C) 2000-2016 by the Free Software Foundation, Inc.
#
# This file is part of GNU Mailman.
#
# GNU Mailman is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see .
"""NNTP runner."""
__all__ = [
'NNTPRunner',
]
import re
import email
import socket
import logging
import nntplib
from io import StringIO
from mailman.config import config
from mailman.core.runner import Runner
from mailman.interfaces.nntp import NewsgroupModeration
COMMA = ','
COMMASPACE = ', '
log = logging.getLogger('mailman.error')
# Matches our Mailman crafted Message-IDs. See Utils.unique_message_id()
# XXX The move to email.utils.make_msgid() breaks this.
mcre = re.compile(r"""
[^@]+) # list's internal_name()
@ # localpart@dom.ain
(?P[^>]+) # list's mail_host
> # trailer
""", re.VERBOSE)
class NNTPRunner(Runner):
def _dispose(self, mlist, msg, msgdata):
# Get NNTP server connection information.
host = config.nntp.host.strip()
port = config.nntp.port.strip()
if len(port) == 0:
port = 119
else:
try:
port = int(port)
except (TypeError, ValueError):
log.exception('Bad [nntp]port value: {0}'.format(port))
port = 119
# Make sure we have the most up-to-date state
if not msgdata.get('prepped'):
prepare_message(mlist, msg, msgdata)
# Flatten the message object, sticking it in a StringIO object
fp = StringIO(msg.as_string())
conn = None
try:
conn = nntplib.NNTP(host, port,
readermode=True,
user=config.nntp.user,
password=config.nntp.password)
conn.post(fp)
except nntplib.NNTPTemporaryError:
log.exception('{0} NNTP error for {1}'.format(
msg.get('message-id', 'n/a'), mlist.fqdn_listname))
except socket.error:
log.exception('{0} NNTP socket error for {1}'.format(
msg.get('message-id', 'n/a'), mlist.fqdn_listname))
except Exception:
# Some other exception occurred, which we definitely did not
# expect, so set this message up for requeuing.
log.exception('{0} NNTP unexpected exception for {1}'.format(
msg.get('message-id', 'n/a'), mlist.fqdn_listname))
return True
finally:
if conn:
conn.quit()
return False
def prepare_message(mlist, msg, msgdata):
# If the newsgroup is moderated, we need to add this header for the Usenet
# software to accept the posting, and not forward it on to the n.g.'s
# moderation address. The posting would not have gotten here if it hadn't
# already been approved. 1 == open list, mod n.g., 2 == moderated
if mlist.newsgroup_moderation in (NewsgroupModeration.open_moderated,
NewsgroupModeration.moderated):
del msg['approved']
msg['Approved'] = mlist.posting_address
# Should we restore the original, non-prefixed subject for gatewayed
# messages? TK: We use stripped_subject (prefix stripped) which was crafted
# in the subject-prefix handler to ensure prefix was stripped from the
# subject came from mailing list user.
stripped_subject = msgdata.get('stripped_subject',
msgdata.get('original_subject'))
if not mlist.nntp_prefix_subject_too and stripped_subject is not None:
del msg['subject']
msg['subject'] = stripped_subject
# Add the appropriate Newsgroups header. Multiple Newsgroups headers are
# generally not allowed so we're not testing for them.
header = msg.get('newsgroups')
if header is None:
msg['Newsgroups'] = mlist.linked_newsgroup
else:
# See if the Newsgroups: header already contains our linked_newsgroup.
# If so, don't add it again. If not, append our linked_newsgroup to
# the end of the header list
newsgroups = [value.strip() for value in header.split(COMMA)]
if mlist.linked_newsgroup not in newsgroups:
newsgroups.append(mlist.linked_newsgroup)
# Subtitute our new header for the old one.
del msg['newsgroups']
msg['Newsgroups'] = COMMASPACE.join(newsgroups)
# Note: We need to be sure two messages aren't ever sent to the same list
# in the same process, since message ids need to be unique. Further, if
# messages are crossposted to two gated mailing lists, they must each have
# unique message ids or the nntpd will only accept one of them. The
# solution here is to substitute any existing message-id that isn't ours
# with one of ours, so we need to parse it to be sure we're not looping.
#
# Our Message-ID format is
#
# XXX 2012-03-31 BAW: What we really want to do is try posting the message
# to the nntpd first, and only if that fails substitute a unique
# Message-ID. The following should get moved out of prepare_message() and
# into _dispose() above.
msgid = msg['message-id']
hackmsgid = True
if msgid:
mo = mcre.search(msgid)
if mo:
lname, hname = mo.group('listname', 'hostname')
if lname == mlist.internal_name() and hname == mlist.mail_host:
hackmsgid = False
if hackmsgid:
del msg['message-id']
msg['Message-ID'] = email.utils.make_msgid()
# Lines: is useful.
if msg['Lines'] is None:
# BAW: is there a better way?
count = len(list(email.iterators.body_line_iterator(msg)))
msg['Lines'] = str(count)
# Massage the message headers by remove some and rewriting others. This
# won't completely sanitize the message, but it will eliminate the bulk of
# the rejections based on message headers. The NNTP server may still
# reject the message because of other problems.
for header in config.nntp.remove_headers.split():
del msg[header]
dup_headers = config.nntp.rewrite_duplicate_headers.split()
if len(dup_headers) % 2 != 0:
# There are an odd number of headers; ignore the last one.
bad_header = dup_headers.pop()
log.error('Ignoring odd [nntp]rewrite_duplicate_headers: {0}'.format(
bad_header))
dup_headers.reverse()
while dup_headers:
source = dup_headers.pop()
target = dup_headers.pop()
values = msg.get_all(source, [])
if len(values) < 2:
# We only care about duplicates.
continue
# Delete all the original headers.
del msg[source]
# Put the first value back on the original header.
msg[source] = values[0]
# And put all the subsequent values on the destination header.
for value in values[1:]:
msg[target] = value
# Mark this message as prepared in case it has to be requeued.
msgdata['prepped'] = True